From 54cbdeec41a1f9f73dc1c8b71b76cf46747e6aeb Mon Sep 17 00:00:00 2001 From: adria0 Date: Fri, 4 Mar 2022 10:16:53 +0100 Subject: [PATCH 01/32] base for ethereum/tests --- .gitmodules | 3 + Cargo.lock | 157 +++++- Cargo.toml | 3 +- eth-types/Cargo.toml | 1 + eth-types/src/geth_types.rs | 27 +- evm-testvectors/Cargo.toml | 28 + evm-testvectors/lllc/Dockerfile | 16 + evm-testvectors/lllc/patch.diff | 15 + evm-testvectors/pre_commit.sh | 5 + evm-testvectors/src/exec.rs | 44 ++ evm-testvectors/src/lllc.rs | 68 +++ evm-testvectors/src/main.rs | 42 ++ evm-testvectors/src/statetest.rs | 157 ++++++ evm-testvectors/src/statetest_yaml.rs | 741 ++++++++++++++++++++++++++ evm-testvectors/test-docker-lllc.sh | 3 + evm-testvectors/tests | 1 + 16 files changed, 1283 insertions(+), 28 deletions(-) create mode 100644 .gitmodules create mode 100644 evm-testvectors/Cargo.toml create mode 100644 evm-testvectors/lllc/Dockerfile create mode 100644 evm-testvectors/lllc/patch.diff create mode 100755 evm-testvectors/pre_commit.sh create mode 100644 evm-testvectors/src/exec.rs create mode 100644 evm-testvectors/src/lllc.rs create mode 100644 evm-testvectors/src/main.rs create mode 100644 evm-testvectors/src/statetest.rs create mode 100644 evm-testvectors/src/statetest_yaml.rs create mode 100755 evm-testvectors/test-docker-lllc.sh create mode 160000 evm-testvectors/tests diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..99f4799129 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "evm-testvectors/tests"] + path = evm-testvectors/tests + url = https://github.com/ethereum/tests diff --git a/Cargo.lock b/Cargo.lock index 38a5310365..d6caa52664 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,9 +50,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.55" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" [[package]] name = "ark-std" @@ -352,9 +352,9 @@ dependencies = [ [[package]] name = "byte-slice-cast" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c751592b77c499e7bce34d99d67c2c11bdc0574e9a488ddade14150a4698" +checksum = "87c5fdd0166095e1d463fc6cc01aa8ce547ad77a4e84d42eb6762b084e28067e" [[package]] name = "byte-tools" @@ -370,9 +370,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytemuck" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" +checksum = "0e851ca7c24871e7336801608a4797d7376545b6928a10d32d75685687141ead" [[package]] name = "byteorder" @@ -857,8 +857,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + +[[package]] +name = "darling" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" +dependencies = [ + "darling_core 0.13.1", + "darling_macro 0.13.1", ] [[package]] @@ -871,7 +881,21 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a340f241d2ceed1deb47ae36c4144b2707ec7dd0b649f894cb39bb595986324" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", "syn", ] @@ -881,7 +905,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ - "darling_core", + "darling_core 0.10.2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" +dependencies = [ + "darling_core 0.13.1", "quote", "syn", ] @@ -920,7 +955,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" dependencies = [ - "darling", + "darling 0.10.2", "derive_builder_core", "proc-macro2", "quote", @@ -933,7 +968,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" dependencies = [ - "darling", + "darling 0.10.2", "proc-macro2", "quote", "syn", @@ -1110,6 +1145,7 @@ dependencies = [ "regex", "serde", "serde_json", + "serde_with", "uint", ] @@ -1373,6 +1409,27 @@ dependencies = [ "walkdir", ] +[[package]] +name = "evm-testvectors" +version = "0.1.0" +dependencies = [ + "anyhow", + "bus-mapping", + "env_logger", + "eth-types", + "ethers-core", + "external-tracer", + "hex", + "k256", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "thiserror", + "yaml-rust", +] + [[package]] name = "expat-sys" version = "2.1.6" @@ -2011,9 +2068,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" +checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" [[package]] name = "itertools" @@ -2096,6 +2153,12 @@ version = "0.2.119" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lock_api" version = "0.4.6" @@ -2318,9 +2381,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" [[package]] name = "oorandom" @@ -2385,7 +2448,7 @@ dependencies = [ [[package]] name = "pairing_bn256" version = "0.1.0" -source = "git+https://github.com/appliedzkp/pairing#230e2c6b84c44e446d97efadc3be233ea610ea00" +source = "git+https://github.com/appliedzkp/pairing#530f4c0022ed0fa430500e837ac7772e84bb4c2c" dependencies = [ "ff 0.11.0", "group 0.11.0", @@ -2659,9 +2722,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dada8c9981fcf32929c3c0f0cd796a9284aca335565227ed88c83babb1d43dc" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ "thiserror", "toml", @@ -2806,9 +2869,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" dependencies = [ "bitflags", ] @@ -2825,9 +2888,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" dependencies = [ "aho-corasick", "memchr", @@ -3013,6 +3076,12 @@ dependencies = [ "base64 0.13.0", ] +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + [[package]] name = "ryu" version = "1.0.9" @@ -3206,6 +3275,29 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1e6ec4d8950e5b1e894eac0d360742f3b1407a6078a604a731c4b3f49cefbc" +dependencies = [ + "rustversion", + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12e47be9471c72889ebafb5e14d5ff930d89ae7a67bbdb5f8abb564f845a927e" +dependencies = [ + "darling 0.13.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "servo-fontconfig" version = "0.5.1" @@ -3358,6 +3450,12 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.1" @@ -3408,9 +3506,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] @@ -4012,6 +4110,15 @@ dependencies = [ "tap", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zeroize" version = "1.4.3" diff --git a/Cargo.toml b/Cargo.toml index 714c6b52db..6887c1892e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ members = [ "eth-types", "external-tracer", "mock", - "prover" + "prover", + "evm-testvectors" ] [patch.crates-io] diff --git a/eth-types/Cargo.toml b/eth-types/Cargo.toml index 530d7fa89f..7fa651ba74 100644 --- a/eth-types/Cargo.toml +++ b/eth-types/Cargo.toml @@ -13,4 +13,5 @@ pairing = { git = 'https://github.com/appliedzkp/pairing', package = "pairing_bn regex = "1.5.4" serde = {version = "1.0.130", features = ["derive"] } serde_json = "1.0.66" +serde_with = "1.12" uint = "0.9.1" diff --git a/eth-types/src/geth_types.rs b/eth-types/src/geth_types.rs index 81ee241f23..e30dd12000 100644 --- a/eth-types/src/geth_types.rs +++ b/eth-types/src/geth_types.rs @@ -1,11 +1,33 @@ //! Types needed for generating Ethereum traces -use crate::{AccessList, Address, Block, Bytes, Error, GethExecTrace, Word, U64}; +use crate::{AccessList, Address, Block, Bytes, Error, GethExecTrace, Word, H256, U64}; use serde::Serialize; +use serde::Serializer; +use serde_with::{serde_as, SerializeAs}; use std::collections::HashMap; +struct WordMapAsH256Map; + +impl SerializeAs> for WordMapAsH256Map { + fn serialize_as(value: &HashMap, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_map(value.iter().map(|(k, v)| { + let mut h256k = H256::zero(); + k.to_big_endian(h256k.as_bytes_mut()); + + let mut h256v = H256::zero(); + v.to_big_endian(h256v.as_bytes_mut()); + + (h256k, h256v) + })) + } +} + /// Definition of all of the data related to an account. -#[derive(Debug, Default, Clone, Serialize)] +#[serde_as] +#[derive(PartialEq, Eq, Debug, Default, Clone, Serialize)] pub struct Account { /// Address pub address: Address, @@ -16,6 +38,7 @@ pub struct Account { /// EVM Code pub code: Bytes, /// Storage + #[serde_as(as = "WordMapAsH256Map")] pub storage: HashMap, } diff --git a/evm-testvectors/Cargo.toml b/evm-testvectors/Cargo.toml new file mode 100644 index 0000000000..49b2d4ebd1 --- /dev/null +++ b/evm-testvectors/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "evm-testvectors" +description="tools for executing the common ethereum tests" +version = "0.1.0" +edition = "2021" + +[dependencies] +bus-mapping = { path = "../bus-mapping" } +eth-types = { path="../eth-types" } +external-tracer = { path="../external-tracer" } +ethers-core = "0.6" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +hex = "0.4.3" +yaml-rust = "0.4.5" +regex = "1" +anyhow = "1" +once_cell = "1.10" +k256 = "0.9" +log = "0.4" +thiserror = "1.0" + +[dev-dependencies] +external-tracer = { path = "../external-tracer" } +env_logger = "0.9" + +[features] +docker-lllc = [] diff --git a/evm-testvectors/lllc/Dockerfile b/evm-testvectors/lllc/Dockerfile new file mode 100644 index 0000000000..a767bec7ca --- /dev/null +++ b/evm-testvectors/lllc/Dockerfile @@ -0,0 +1,16 @@ +FROM alpine AS build + +WORKDIR /solidity + +RUN apk update && apk add boost-dev boost-static build-base cmake git + +RUN git clone https://github.com/ethereum/solidity . +RUN git checkout 8f2595957bfc0f3cd18ca29240dabcd6b2122dfd +COPY patch.diff patch.diff +RUN patch -p1 < patch.diff + +WORKDIR /solidity/build +RUN cmake .. -DCMAKE_BUILD_TYPE=Release -DLLL=1 +RUN make -j2 + +ENTRYPOINT ["/solidity/build/lllc/lllc"] diff --git a/evm-testvectors/lllc/patch.diff b/evm-testvectors/lllc/patch.diff new file mode 100644 index 0000000000..bf3cfa4d44 --- /dev/null +++ b/evm-testvectors/lllc/patch.diff @@ -0,0 +1,15 @@ +diff --git a/liblll/Parser.cpp b/liblll/Parser.cpp +index 3b68bc2da..06a3eb067 100644 +--- a/liblll/Parser.cpp ++++ b/liblll/Parser.cpp +@@ -67,8 +67,8 @@ void dev::lll::debugOutAST(ostream& _out, sp::utree const& _this) + + break; + case sp::utree_type::int_type: _out << _this.get(); break; +- case sp::utree_type::string_type: _out << "\"" << _this.get, sp::utree_type::string_type>>() << "\""; break; +- case sp::utree_type::symbol_type: _out << _this.get, sp::utree_type::symbol_type>>(); break; ++ // case sp::utree_type::string_type: _out << "\"" << _this.get, sp::utree_type::string_type>>() << "\""; break; ++ // case sp::utree_type::symbol_type: _out << _this.get, sp::utree_type::symbol_type>>(); break; + case sp::utree_type::any_type: _out << *_this.get(); break; + default: _out << "nil"; + } diff --git a/evm-testvectors/pre_commit.sh b/evm-testvectors/pre_commit.sh new file mode 100755 index 0000000000..e14be593e1 --- /dev/null +++ b/evm-testvectors/pre_commit.sh @@ -0,0 +1,5 @@ +cargo fmt +cargo clippy -- -Dwarnings -W clippy::pedantic +cargo update +cargo test +cargo outdated --root-deps-only diff --git a/evm-testvectors/src/exec.rs b/evm-testvectors/src/exec.rs new file mode 100644 index 0000000000..8734a0c2bb --- /dev/null +++ b/evm-testvectors/src/exec.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use bus_mapping::circuit_input_builder::CircuitInputBuilder; +use bus_mapping::mock::BlockData; +use external_tracer::TraceConfig; + +use eth_types::U64; +pub fn traceconfig(trace_config: TraceConfig) -> Result { + // get the geth traces + let geth_trace = external_tracer::trace(&trace_config)?; + + // process the transaction + let geth_data = eth_types::geth_types::GethData { + chain_id: trace_config.chain_id, + history_hashes: trace_config.history_hashes, + eth_block: eth_types::Block { + author: trace_config.block_constants.coinbase, + timestamp: trace_config.block_constants.timestamp, + number: Some(U64::from(trace_config.block_constants.timestamp.as_u64())), + difficulty: trace_config.block_constants.difficulty, + gas_limit: trace_config.block_constants.gas_limit, + base_fee_per_gas: Some(trace_config.block_constants.base_fee), + ..eth_types::Block::default() + }, + eth_tx: eth_types::Transaction { + from: trace_config.transaction.from, + to: trace_config.transaction.to, + value: trace_config.transaction.value, + input: trace_config.transaction.call_data, + gas_price: Some(trace_config.transaction.gas_price), + access_list: trace_config.transaction.access_list, + nonce: trace_config.transaction.nonce, + gas: trace_config.transaction.gas_limit, + transaction_index: Some(U64::zero()), + ..eth_types::Transaction::default() + }, + geth_trace, + accounts: trace_config.accounts.into_values().collect(), + }; + + let block_data = BlockData::new_from_geth_data(geth_data); + let mut builder = block_data.new_circuit_input_builder(); + builder.handle_tx(&block_data.eth_tx, &block_data.geth_trace)?; + Ok(builder) +} diff --git a/evm-testvectors/src/lllc.rs b/evm-testvectors/src/lllc.rs new file mode 100644 index 0000000000..0c28846fc5 --- /dev/null +++ b/evm-testvectors/src/lllc.rs @@ -0,0 +1,68 @@ +use anyhow::{bail, Context, Result}; +use eth_types::Bytes; +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +pub struct Lllc { + path: PathBuf, + args: Vec, +} + +/// Create a new builder from the lllc path +/// there is some problems compiling old solidity version in M1 +/// use patched github.com/adria0/solidity commit +/// 931b8a66cb985476b5c61e23d41e030c0cff009a +impl Lllc { + pub fn new(path: PathBuf, args: &[&str]) -> Self { + Self { + path, + args: args.iter().map(ToString::to_string).collect(), + } + } + pub fn with_docker() -> Self { + Lllc::new(PathBuf::from("docker"), &["run", "-i", "--rm", "lllc"]) + } + + /// compiles LLL code + pub fn compile(&self, src: &str) -> Result { + let mut child = Command::new(self.path.clone()) + .args(self.args.clone()) + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + + child + .stdin + .as_mut() + .context("failed to open stdin")? + .write_all(src.as_bytes())?; + + let output = child.wait_with_output()?; + + if output.status.success() { + let raw_output = String::from_utf8(output.stdout)?; + log::trace!(target:"evmvectests", "lllc out={}",raw_output); + Ok(Bytes::from(hex::decode(raw_output.trim())?)) + } else { + let err = String::from_utf8(output.stderr)?; + bail!("lllc command failed {:?}", err) + } + } +} + +#[cfg(test)] +mod test { + #[test] + #[cfg(feature = "docker-lllc")] + fn test_docker_lllc() -> anyhow::Result<()> { + let out = super::Lllc::with_docker().compile( + "[[0]] (+ 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 4)", + )?; + assert_eq!( + hex::encode(out), + "60047fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0160005500" + ); + Ok(()) + } +} diff --git a/evm-testvectors/src/main.rs b/evm-testvectors/src/main.rs new file mode 100644 index 0000000000..b5859cfd7b --- /dev/null +++ b/evm-testvectors/src/main.rs @@ -0,0 +1,42 @@ +mod exec; +mod lllc; +mod statetest; +mod statetest_yaml; + +use crate::lllc::Lllc; +use crate::statetest_yaml::YamlStateTestBuilder; +use anyhow::{bail, Result}; + +/// This crate helps to execute the common ethereum tests located in https://github.com/ethereum/tests + +/// # Errors +fn run_yaml_state_tests(yaml: &str, lllc: Option) -> Result<()> { + let mut failed = 0; + + // generate all combinations of tests specified in the yaml + let tcs = YamlStateTestBuilder::new(lllc).from_yaml(yaml)?; + + // for each test + for tc in tcs { + let id = tc.id.to_string(); + if let Some(err) = tc.run().err() { + log::error!(target: "vmvectests", "FAILED test {} : {}",id, err); + failed += 1; + } else { + log::info!(target: "vmvectests", "OK test {}", id); + } + } + + if failed > 0 { + bail!("{} tests failed", failed); + } + Ok(()) +} + +fn main() -> Result<()>{ + run_yaml_state_tests( + include_str!("../tests/src/GeneralStateTestsFiller/VMTests/vmArithmeticTest/addFiller.yml"), + Some(lllc::Lllc::with_docker()), + )?; + Ok(()) +} diff --git a/evm-testvectors/src/statetest.rs b/evm-testvectors/src/statetest.rs new file mode 100644 index 0000000000..3b00804eff --- /dev/null +++ b/evm-testvectors/src/statetest.rs @@ -0,0 +1,157 @@ +use anyhow::Context; +use eth_types::{geth_types, geth_types::Account, Address, Bytes, H256, U256, U64}; +use external_tracer::TraceConfig; +use std::collections::HashMap; +use thiserror::Error; + +#[derive(PartialEq, Eq, Error, Debug)] +pub enum StateTestError { + #[error("cannot generate circuit input: `{0}`")] + CircuitInput(String), + #[error("balance mismatch (expected {expected:?}, found {found:?})")] + BalanceMismatch { expected: U256, found: U256 }, + #[error("nonce mismatch (expected {expected:?}, found {found:?})")] + NonceMismatch { expected: U256, found: U256 }, + #[error("code mismatch (expected {expected:?}, found {found:?})")] + CodeMismatch { expected: Bytes, found: Bytes }, + #[error("storage mismatch slot={} (expected {expected:?}, found {found:?})")] + StorageMismatch { + slot: U256, + expected: U256, + found: Option, + }, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct Env { + pub current_coinbase: Address, + pub current_difficulty: U256, + pub current_gas_limit: u64, + pub current_number: u64, + pub current_timestamp: u64, + pub previous_hash: H256, +} + +#[derive(PartialEq, Eq, Default, Debug, Clone)] +pub struct PartialAccount { + pub address: Address, + pub balance: Option, + pub code: Option, + pub nonce: Option, + pub storage: HashMap, +} + +impl TryInto for PartialAccount { + type Error = anyhow::Error; + fn try_into(self) -> Result { + Ok(Account { + address: self.address, + balance: self.balance.context("balance")?, + code: self.code.context("code")?, + nonce: self.nonce.context("nonce")?, + storage: self.storage, + }) + } +} + +type StateTestResult = HashMap; + +#[derive(PartialEq, Eq, Debug)] +pub struct StateTest { + pub id: String, + pub env: Env, + pub secret_key: Bytes, + pub from: Address, + pub to: Option
, + pub gas_limit: u64, + pub gas_price: U256, + pub nonce: U256, + pub value: U256, + pub data: Bytes, + pub pre: HashMap, + pub result: StateTestResult, +} + +impl StateTest { + fn into_traceconfig(self) -> (String, TraceConfig, StateTestResult) { + ( + self.id, + TraceConfig { + chain_id: U256::one(), + history_hashes: Vec::new(), + block_constants: geth_types::BlockConstants { + coinbase: self.env.current_coinbase, + timestamp: U256::from(self.env.current_timestamp), + number: U64::from(self.env.current_number), + difficulty: self.env.current_difficulty, + gas_limit: U256::from(self.env.current_gas_limit), + base_fee: U256::one(), + }, + transaction: geth_types::Transaction { + from: self.from, + to: self.to, + nonce: self.nonce, + value: self.value, + gas_limit: U256::from(self.gas_limit), + gas_price: self.gas_price, + gas_fee_cap: U256::zero(), + gas_tip_cap: U256::zero(), + call_data: self.data, + access_list: None, + }, + accounts: self.pre, + }, + self.result, + ) + } + pub fn run(self) -> Result<(), StateTestError> { + // get the geth traces + let (_, trace_config, post) = self.into_traceconfig(); + let builder = crate::exec::traceconfig(trace_config) + .map_err(|err| StateTestError::CircuitInput(err.to_string()))?; + + // check if the generated account data is the expected one + for (address, expected) in post { + let (_, actual) = builder.sdb.get_account(&address); + + if expected.balance.map(|v| v == actual.balance) == Some(false) { + return Err(StateTestError::BalanceMismatch { + expected: expected.balance.unwrap(), + found: actual.balance, + }); + } + + if expected.nonce.map(|v| v == actual.nonce) == Some(false) { + return Err(StateTestError::NonceMismatch { + expected: expected.nonce.unwrap(), + found: actual.nonce, + }); + } + + if let Some(expected_code) = expected.code { + let actual_code = if actual.code_hash.is_zero() { + std::borrow::Cow::Owned(Vec::new()) + } else { + std::borrow::Cow::Borrowed(&builder.code_db.0[&actual.code_hash]) + }; + if &actual_code as &[u8] != expected_code.0 { + return Err(StateTestError::CodeMismatch { + expected: expected_code, + found: Bytes::from(actual_code.to_vec()), + }); + } + } + for (slot, expected_value) in expected.storage { + let actual_value = actual.storage.get(&slot); + if Some(&expected_value) != actual_value { + return Err(StateTestError::StorageMismatch { + slot, + expected: expected_value, + found: actual_value.copied(), + }); + } + } + } + Ok(()) + } +} diff --git a/evm-testvectors/src/statetest_yaml.rs b/evm-testvectors/src/statetest_yaml.rs new file mode 100644 index 0000000000..91290209e8 --- /dev/null +++ b/evm-testvectors/src/statetest_yaml.rs @@ -0,0 +1,741 @@ +use super::lllc::Lllc; +use crate::statetest::{Env, PartialAccount, StateTest}; +use anyhow::{bail, Context, Result}; +use eth_types::{geth_types::Account, Address, Bytes, H256, U256}; +use ethers_core::utils::secret_key_to_address; +use k256::ecdsa::SigningKey; +use once_cell::sync::Lazy; +use regex::Regex; +use std::collections::HashMap; +use std::convert::TryInto; +use yaml_rust::Yaml; + +static TAGS_REGEXP: Lazy = Lazy::new(|| Regex::new("((:[a-z]+ )([^:]+))").unwrap()); + +type Tag = String; +type Label = String; + +#[derive(Debug, Clone)] +enum Ref { + Any, + Index(usize), + Label(String), +} + +struct Refs(Vec); + +impl Refs { + fn contains_index(&self, idx: usize) -> bool { + self.0.iter().any(|r| match r { + Ref::Index(i) => i == &idx, + Ref::Label(_) => false, + Ref::Any => true, + }) + } + fn contains_label(&self, lbl: &str) -> bool { + self.0.iter().any(|r| match r { + Ref::Index(_) => false, + Ref::Label(l) => l == lbl, + Ref::Any => true, + }) + } +} + +pub struct YamlStateTestBuilder { + lllc: Option, +} + +impl YamlStateTestBuilder { + pub fn new(lllc: Option) -> Self { + Self { lllc } + } + + /// generates `StateTest` vectors from a ethereum yaml test specification + pub fn from_yaml(&self, source: &str) -> Result> { + // get the yaml root element + let doc = yaml_rust::YamlLoader::load_from_str(source)? + .into_iter() + .next() + .context("get yaml doc")?; + + // collect test names, that are the top-level items in the yaml doc + let test_names: Vec<_> = doc + .as_hash() + .context("parse_hash")? + .keys() + .map(|v| v.as_str().context("as_str")) + .collect::>()?; + + // for each test defined in the yaml, create the according defined tests + let mut tests = Vec::new(); + for test_name in test_names { + let yaml_test = &doc[test_name]; + + // parse env + let env = Self::parse_env(&yaml_test["env"])?; + + // parse pre (account states before executing the transaction) + // [TODO] remove ugly unwrap here + let pre: HashMap = self + .parse_accounts(&yaml_test["pre"])? + .into_iter() + .map(|(addr, account)| (addr, account.try_into().unwrap())) + .collect(); + + // parse transaction + let yaml_transaction = &yaml_test["transaction"]; + let data_s: Vec<_> = yaml_transaction["data"] + .as_vec() + .context("as_vec")? + .iter() + .map(Self::parse_calldata) + .collect::>()?; + + let gas_limit_s: Vec<_> = yaml_transaction["gasLimit"] + .as_vec() + .context("as_vec")? + .iter() + .map(Self::parse_u64) + .collect::>()?; + + let value_s: Vec<_> = yaml_transaction["value"] + .as_vec() + .context("as_vec")? + .iter() + .map(Self::parse_u256) + .collect::>()?; + + let gas_price = Self::parse_u256(&yaml_transaction["gasPrice"])?; + let nonce = Self::parse_u256(&yaml_transaction["nonce"])?; + let to = Self::parse_address(&yaml_transaction["to"])?; + let secret_key = Self::parse_bytes(&yaml_transaction["secretKey"])?; + let from = secret_key_to_address(&SigningKey::from_bytes(&secret_key.to_vec())?); + + // parse expects (account states before executing the transaction) + let mut expects = Vec::new(); + for expect in yaml_test["expect"].as_vec().context("as_vec")?.iter() { + let data_refs = Self::parse_refs(&expect["indexes"]["data"])?; + let gparse_refs = Self::parse_refs(&expect["indexes"]["gas"])?; + let value_refs = Self::parse_refs(&expect["indexes"]["value"])?; + let result = self.parse_accounts(&expect["result"])?; + expects.push((data_refs, gparse_refs, value_refs, result)); + } + + // generate all the tests defined in the transaction by generating product of + // data x gas x value + for (idx_data, data) in data_s.iter().enumerate() { + for (idx_gas, gas_limit) in gas_limit_s.iter().enumerate() { + for (idx_value, value) in value_s.iter().enumerate() { + // find the first result that fulfills the pattern + for (data_refs, parse_refs, value_refs, result) in &expects { + // check if this result can be applied to the current test + let mut data_label = String::new(); + if let Some(label) = &data.1 { + if !data_refs.contains_label(label) { + continue; + } + data_label = format!("({})", label); + } else if !data_refs.contains_index(idx_data) { + continue; + } + + if !parse_refs.contains_index(idx_gas) { + continue; + } + + if !value_refs.contains_index(idx_value) { + continue; + } + + // add the test + tests.push(StateTest { + id: format!( + "{}_d{}{}_g{}_v{}", + test_name, idx_data, data_label, idx_gas, idx_value + ), + env: env.clone(), + pre: pre.clone(), + result: result.clone(), + from, + secret_key: secret_key.clone(), + to: Some(to), + gas_limit: *gas_limit, + gas_price, + nonce, + value: *value, + data: data.0.clone(), + }); + break; + } + } + } + } + } + + Ok(tests) + } + + /// parse env section + fn parse_env(yaml: &Yaml) -> Result { + Ok(Env { + current_coinbase: Self::parse_address(&yaml["currentCoinbase"])?, + current_difficulty: Self::parse_u256(&yaml["currentDifficulty"])?, + current_gas_limit: Self::parse_u64(&yaml["currentGasLimit"])?, + current_number: Self::parse_u64(&yaml["currentNumber"])?, + current_timestamp: Self::parse_u64(&yaml["currentTimestamp"])?, + previous_hash: Self::parse_hash(&yaml["previousHash"])?, + }) + } + + /// parse a vector of address=>(storage,balance,code,nonce) entry + fn parse_accounts(&self, yaml: &Yaml) -> Result> { + let mut accounts = HashMap::new(); + for (address, account) in yaml.as_hash().context("parse_hash")?.iter() { + let acc_storage = &account["storage"]; + let acc_balance = &account["balance"]; + let acc_code = &account["code"]; + let acc_nonce = &account["nonce"]; + + let mut storage = HashMap::new(); + if !acc_storage.is_badvalue() { + for (slot, value) in account["storage"].as_hash().context("parse_hash")?.iter() { + storage.insert(Self::parse_u256(slot)?, Self::parse_u256(value)?); + } + } + + let address = Self::parse_address(address)?; + let account = PartialAccount { + address, + balance: if acc_balance.is_badvalue() { + None + } else { + Some(Self::parse_u256(acc_balance)?) + }, + code: if acc_code.is_badvalue() { + None + } else { + Some(self.parse_code(acc_code)?) + }, + nonce: if acc_nonce.is_badvalue() { + None + } else { + Some(Self::parse_u256(acc_nonce)?) + }, + storage, + }; + accounts.insert(address, account); + } + Ok(accounts) + } + + /// converts list of tagged values string into a map + /// if there's no tags, an entry with an empty tag and the full string is + /// returned + fn decompose_tags(expr: &str) -> HashMap { + let expr = expr.trim(); + if expr.starts_with(':') { + TAGS_REGEXP + .captures_iter(expr) + .map(|cap| (cap[2].trim().into(), cap[3].trim().into())) + .collect() + } else { + let mut tags = HashMap::new(); + tags.insert("".to_string(), expr.to_string()); + tags + } + } + + /// returns the element as an address + fn parse_address(yaml: &Yaml) -> Result
{ + if let Some(as_str) = yaml.as_str() { + Ok(Address::from_slice(&hex::decode(as_str)?)) + } else if let Some(as_i64) = yaml.as_i64() { + let hex = format!("{:0>40}", as_i64); + Ok(Address::from_slice(&hex::decode(hex)?)) + } else { + bail!("cannot address"); + } + } + + /// returns the element as an array of bytes + fn parse_bytes(yaml: &Yaml) -> Result { + let mut as_str = yaml.as_str().context("as_str")?; + if let Some(stripped) = as_str.strip_prefix("0x") { + as_str = stripped; + } + Ok(Bytes::from(hex::decode(as_str)?)) + } + + /// returns the element as calldata bytes, supports :raw and :abi + fn parse_calldata(yaml: &Yaml) -> Result<(Bytes, Option