mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
refactor: move ef tests to own testing crate (#2847)
This commit is contained in:
12
.github/workflows/unit.yml
vendored
12
.github/workflows/unit.yml
vendored
@ -58,14 +58,15 @@ jobs:
|
||||
RUST_LOG: info,sync=error
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Checkout ethereum/tests
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: ethereum/tests
|
||||
path: ethtests
|
||||
path: testing/ef-tests/ethereum-tests
|
||||
submodules: recursive
|
||||
depth: 1
|
||||
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
@ -73,13 +74,14 @@ jobs:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Install latest nextest release
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run Ethereum tests
|
||||
run: cargo run --release -- test-chain ethtests/BlockchainTests/GeneralStateTests/
|
||||
run: cargo nextest run --release -p ef-tests --features ef-tests
|
||||
|
||||
doc-test:
|
||||
name: rustdoc
|
||||
|
||||
35
Cargo.lock
generated
35
Cargo.lock
generated
@ -1544,6 +1544,24 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ef-tests"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"reth-db",
|
||||
"reth-interfaces",
|
||||
"reth-primitives",
|
||||
"reth-provider",
|
||||
"reth-revm",
|
||||
"reth-rlp",
|
||||
"reth-stages",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.1"
|
||||
@ -5980,18 +5998,18 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.160"
|
||||
version = "1.0.163"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
|
||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.160"
|
||||
version = "1.0.163"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df"
|
||||
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.56",
|
||||
"quote 1.0.26",
|
||||
@ -6000,9 +6018,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.94"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
|
||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -7337,12 +7355,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.2"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
|
||||
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
|
||||
@ -42,6 +42,7 @@ members = [
|
||||
"crates/tasks",
|
||||
"crates/transaction-pool",
|
||||
"crates/trie",
|
||||
"testing/ef-tests"
|
||||
]
|
||||
default-members = ["bin/reth"]
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ use crate::{
|
||||
dirs::{LogsDir, PlatformPath},
|
||||
merkle_debug, node, p2p,
|
||||
runner::CliRunner,
|
||||
stage, test_eth_chain, test_vectors,
|
||||
stage, test_vectors,
|
||||
version::{LONG_VERSION, SHORT_VERSION},
|
||||
};
|
||||
use clap::{ArgAction, Args, Parser, Subcommand};
|
||||
@ -35,7 +35,6 @@ pub fn run() -> eyre::Result<()> {
|
||||
Commands::Stage(command) => runner.run_blocking_until_ctrl_c(command.execute()),
|
||||
Commands::P2P(command) => runner.run_until_ctrl_c(command.execute()),
|
||||
Commands::TestVectors(command) => runner.run_until_ctrl_c(command.execute()),
|
||||
Commands::TestEthChain(command) => runner.run_until_ctrl_c(command.execute()),
|
||||
Commands::Config(command) => runner.run_until_ctrl_c(command.execute()),
|
||||
Commands::MerkleDebug(command) => runner.run_until_ctrl_c(command.execute()),
|
||||
}
|
||||
@ -62,9 +61,6 @@ pub enum Commands {
|
||||
/// P2P Debugging utilities
|
||||
#[command(name = "p2p")]
|
||||
P2P(p2p::Command),
|
||||
/// Run Ethereum blockchain tests
|
||||
#[command(name = "test-chain")]
|
||||
TestEthChain(test_eth_chain::Command),
|
||||
/// Generate Test Vectors
|
||||
#[command(name = "test-vectors")]
|
||||
TestVectors(test_vectors::Command),
|
||||
|
||||
@ -19,7 +19,6 @@ pub mod p2p;
|
||||
pub mod prometheus_exporter;
|
||||
pub mod runner;
|
||||
pub mod stage;
|
||||
pub mod test_eth_chain;
|
||||
pub mod test_vectors;
|
||||
pub mod utils;
|
||||
pub mod version;
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
//! Command for running Ethereum chain tests.
|
||||
|
||||
use clap::Parser;
|
||||
use eyre::eyre;
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{error, info, warn};
|
||||
/// Models for parsing JSON blockchain tests
|
||||
pub mod models;
|
||||
/// Ethereum blockhain test runner
|
||||
pub mod runner;
|
||||
|
||||
use runner::TestOutcome;
|
||||
|
||||
/// Execute Ethereum blockchain tests by specifying path to json files
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
/// Path to Ethereum JSON test files
|
||||
path: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Execute the command
|
||||
pub async fn execute(self) -> eyre::Result<()> {
|
||||
let mut futs: FuturesUnordered<_> = self
|
||||
.path
|
||||
.iter()
|
||||
.flat_map(|item| reth_staged_sync::utils::find_all_files_with_postfix(item, ".json"))
|
||||
.map(|file| async { (runner::run_test(file.clone()).await, file) })
|
||||
.collect();
|
||||
|
||||
let mut failed = 0;
|
||||
let mut passed = 0;
|
||||
let mut skipped = 0;
|
||||
while let Some((result, file)) = futs.next().await {
|
||||
match TestOutcome::from(result) {
|
||||
TestOutcome::Passed => {
|
||||
info!(target: "reth::cli", "[+] Test {file:?} passed.");
|
||||
passed += 1;
|
||||
}
|
||||
TestOutcome::Skipped => {
|
||||
warn!(target: "reth::cli", "[=] Test {file:?} skipped.");
|
||||
skipped += 1;
|
||||
}
|
||||
TestOutcome::Failed(error) => {
|
||||
error!(target: "reth::cli", "Test {file:?} failed:\n{error}");
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(target: "reth::cli", "{passed}/{0} tests passed, {skipped}/{0} skipped, {failed}/{0} failed.\n", failed + passed + skipped);
|
||||
|
||||
if failed != 0 {
|
||||
Err(eyre!("Failed {failed} tests"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,303 +0,0 @@
|
||||
use super::models::Test;
|
||||
use crate::test_eth_chain::models::{ForkSpec, RootOrState};
|
||||
use eyre::eyre;
|
||||
use reth_db::{
|
||||
cursor::DbCursorRO,
|
||||
database::Database,
|
||||
mdbx::test_utils::create_test_rw_db,
|
||||
tables,
|
||||
transaction::{DbTx, DbTxMut},
|
||||
DatabaseError as DbError,
|
||||
};
|
||||
use reth_primitives::{
|
||||
keccak256, Account as RethAccount, Address, Bytecode, ChainSpec, JsonU256, SealedBlock,
|
||||
SealedHeader, StageCheckpoint, StorageEntry, H256, U256,
|
||||
};
|
||||
use reth_provider::Transaction;
|
||||
use reth_rlp::Decodable;
|
||||
use reth_stages::{stages::ExecutionStage, ExecInput, Stage, StageId};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
/// The outcome of a test.
|
||||
#[derive(Debug)]
|
||||
pub enum TestOutcome {
|
||||
/// The test was skipped.
|
||||
Skipped,
|
||||
/// The test passed.
|
||||
Passed,
|
||||
/// The test failed.
|
||||
Failed(eyre::Report),
|
||||
}
|
||||
|
||||
impl From<eyre::Result<TestOutcome>> for TestOutcome {
|
||||
fn from(v: eyre::Result<TestOutcome>) -> TestOutcome {
|
||||
match v {
|
||||
Ok(outcome) => outcome,
|
||||
Err(err) => TestOutcome::Failed(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests are test edge cases that are not possible to happen on mainnet, so we are skipping them.
|
||||
pub fn should_skip(path: &Path) -> bool {
|
||||
// funky test with `bigint 0x00` value in json :) not possible to happen on mainnet and require
|
||||
// custom json parser. https://github.com/ethereum/tests/issues/971
|
||||
if path.file_name() == Some(OsStr::new("ValueOverflow.json")) {
|
||||
return true
|
||||
}
|
||||
// txbyte is of type 02 and we dont parse tx bytes for this test to fail.
|
||||
if path.file_name() == Some(OsStr::new("typeTwoBerlin.json")) {
|
||||
return true
|
||||
}
|
||||
// Test checks if nonce overflows. We are handling this correctly but we are not parsing
|
||||
// exception in testsuite There are more nonce overflow tests that are in internal
|
||||
// call/create, and those tests are passing and are enabled.
|
||||
if path.file_name() == Some(OsStr::new("CreateTransactionHighNonce.json")) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Test check if gas price overflows, we handle this correctly but does not match tests specific
|
||||
// exception.
|
||||
if path.file_name() == Some(OsStr::new("HighGasPrice.json")) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip test where basefee/accesslist/difficulty is present but it shouldn't be supported in
|
||||
// London/Berlin/TheMerge. https://github.com/ethereum/tests/blob/5b7e1ab3ffaf026d99d20b17bb30f533a2c80c8b/GeneralStateTests/stExample/eip1559.json#L130
|
||||
// It is expected to not execute these tests.
|
||||
if path.file_name() == Some(OsStr::new("accessListExample.json")) ||
|
||||
path.file_name() == Some(OsStr::new("basefeeExample.json")) ||
|
||||
path.file_name() == Some(OsStr::new("eip1559.json")) ||
|
||||
path.file_name() == Some(OsStr::new("mergeTest.json"))
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
// These tests are passing, but they take a lot of time to execute so we are going to skip them.
|
||||
if path.file_name() == Some(OsStr::new("loopExp.json")) ||
|
||||
path.file_name() == Some(OsStr::new("Call50000_sha256.json")) ||
|
||||
path.file_name() == Some(OsStr::new("static_Call50000_sha256.json")) ||
|
||||
path.file_name() == Some(OsStr::new("loopMul.json")) ||
|
||||
path.file_name() == Some(OsStr::new("CALLBlake2f_MaxRounds.json")) ||
|
||||
path.file_name() == Some(OsStr::new("shiftCombinations.json"))
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
// Ignore outdated EOF tests that haven't been updated for Cancun yet.
|
||||
let eof_path = Path::new("EIPTests").join("stEOF");
|
||||
if path.to_string_lossy().contains(&*eof_path.to_string_lossy()) {
|
||||
return true
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Run one JSON-encoded Ethereum blockchain test at the specified path.
|
||||
pub async fn run_test(path: PathBuf) -> eyre::Result<TestOutcome> {
|
||||
let path = path.as_path();
|
||||
let json_file = std::fs::read(path)?;
|
||||
let suites: Test = serde_json::from_reader(&*json_file)?;
|
||||
|
||||
if should_skip(path) {
|
||||
return Ok(TestOutcome::Skipped)
|
||||
}
|
||||
|
||||
debug!(target: "reth::cli", ?path, "Running test suite");
|
||||
|
||||
for (name, suite) in suites.0 {
|
||||
if matches!(
|
||||
suite.network,
|
||||
ForkSpec::ByzantiumToConstantinopleAt5 |
|
||||
ForkSpec::Constantinople |
|
||||
ForkSpec::ConstantinopleFix |
|
||||
ForkSpec::MergeEOF |
|
||||
ForkSpec::MergeMeterInitCode |
|
||||
ForkSpec::MergePush0 |
|
||||
ForkSpec::Unknown
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
let pre_state = suite.pre.0;
|
||||
|
||||
debug!(target: "reth::cli", name, network = ?suite.network, "Running test");
|
||||
|
||||
let chain_spec: ChainSpec = suite.network.into();
|
||||
|
||||
// Create db and acquire transaction
|
||||
let db = create_test_rw_db();
|
||||
let tx = db.tx_mut()?;
|
||||
|
||||
// insert genesis
|
||||
let header: SealedHeader = suite.genesis_block_header.into();
|
||||
let genesis_block = SealedBlock { header, body: vec![], ommers: vec![], withdrawals: None };
|
||||
reth_provider::insert_canonical_block(&tx, genesis_block, None)?;
|
||||
|
||||
let mut last_block = None;
|
||||
suite.blocks.iter().try_for_each(|block| -> eyre::Result<()> {
|
||||
let decoded = SealedBlock::decode(&mut block.rlp.as_ref())?;
|
||||
last_block = Some(decoded.number);
|
||||
reth_provider::insert_canonical_block(&tx, decoded, None)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
pre_state.into_iter().try_for_each(|(address, account)| -> eyre::Result<()> {
|
||||
let has_code = !account.code.is_empty();
|
||||
let code_hash = has_code.then(|| keccak256(&account.code));
|
||||
tx.put::<tables::PlainAccountState>(
|
||||
address,
|
||||
RethAccount {
|
||||
balance: account.balance.0,
|
||||
nonce: account.nonce.0.to::<u64>(),
|
||||
bytecode_hash: code_hash,
|
||||
},
|
||||
)?;
|
||||
if let Some(code_hash) = code_hash {
|
||||
tx.put::<tables::Bytecodes>(code_hash, Bytecode::new_raw(account.code.0))?;
|
||||
}
|
||||
account.storage.iter().try_for_each(|(k, v)| {
|
||||
trace!(target: "reth::cli", ?address, key = ?k.0, value = ?v.0, "Update storage");
|
||||
tx.put::<tables::PlainStorageState>(
|
||||
address,
|
||||
StorageEntry { key: H256::from_slice(&k.0.to_be_bytes::<32>()), value: v.0 },
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Commit the pre suite state
|
||||
tx.commit()?;
|
||||
|
||||
let storage = db.view(|tx| -> Result<_, DbError> {
|
||||
let mut cursor = tx.cursor_dup_read::<tables::PlainStorageState>()?;
|
||||
let walker = cursor.first()?.map(|first| cursor.walk(Some(first.0))).transpose()?;
|
||||
Ok(walker.map(|mut walker| {
|
||||
let mut map: HashMap<Address, HashMap<U256, U256>> = HashMap::new();
|
||||
while let Some(Ok((address, slot))) = walker.next() {
|
||||
let key = U256::from_be_bytes(slot.key.0);
|
||||
map.entry(address).or_default().insert(key, slot.value);
|
||||
}
|
||||
map
|
||||
}))
|
||||
})??;
|
||||
trace!(target: "reth::cli", ?storage, "Pre-state");
|
||||
|
||||
// Initialize the execution stage
|
||||
// Hardcode the chain_id to Ethereum 1.
|
||||
let factory = reth_revm::Factory::new(Arc::new(chain_spec));
|
||||
let mut stage = ExecutionStage::new_with_factory(factory);
|
||||
|
||||
// Call execution stage
|
||||
let input = ExecInput {
|
||||
previous_stage: last_block.map(|b| (StageId(""), StageCheckpoint::new(b))),
|
||||
checkpoint: None,
|
||||
};
|
||||
{
|
||||
let mut transaction = Transaction::new(db.as_ref())?;
|
||||
|
||||
// ignore error
|
||||
let _ = stage.execute(&mut transaction, input).await;
|
||||
transaction.commit()?;
|
||||
}
|
||||
|
||||
// Validate post state
|
||||
match suite.post_state {
|
||||
Some(RootOrState::Root(root)) => {
|
||||
debug!(target: "reth::cli", "Post-state root: #{root:?}")
|
||||
}
|
||||
Some(RootOrState::State(state)) => db.view(|tx| -> eyre::Result<()> {
|
||||
let mut cursor = tx.cursor_dup_read::<tables::PlainStorageState>()?;
|
||||
let walker = cursor.first()?.map(|first| cursor.walk(Some(first.0))).transpose()?;
|
||||
let storage = walker.map(|mut walker| {
|
||||
let mut map: HashMap<Address, HashMap<U256, U256>> = HashMap::new();
|
||||
while let Some(Ok((address, slot))) = walker.next() {
|
||||
let key = U256::from_be_bytes(slot.key.0);
|
||||
map.entry(address).or_default().insert(key, slot.value);
|
||||
}
|
||||
map
|
||||
});
|
||||
tracing::trace!("Our storage:{:?}", storage);
|
||||
for (address, test_account) in state.iter() {
|
||||
// check account
|
||||
let our_account =
|
||||
tx.get::<tables::PlainAccountState>(*address)?.ok_or_else(|| {
|
||||
eyre!("Account is missing: {address} expected: {:?}", test_account)
|
||||
})?;
|
||||
if test_account.balance.0 != our_account.balance {
|
||||
return Err(eyre!(
|
||||
"Account {address} balance diff, expected {} got {}",
|
||||
test_account.balance.0,
|
||||
our_account.balance
|
||||
))
|
||||
}
|
||||
if test_account.nonce.0.to::<u64>() != our_account.nonce {
|
||||
return Err(eyre!(
|
||||
"Account {address} nonce diff, expected {} got {}",
|
||||
test_account.nonce.0,
|
||||
our_account.nonce
|
||||
))
|
||||
}
|
||||
if let Some(our_bytecode) = our_account.bytecode_hash {
|
||||
let test_bytecode = keccak256(test_account.code.as_ref());
|
||||
if our_bytecode != test_bytecode {
|
||||
return Err(eyre!(
|
||||
"Account {address} bytecode diff, expected: {} got: {:?}",
|
||||
test_account.code,
|
||||
our_account.bytecode_hash
|
||||
))
|
||||
}
|
||||
} else if !test_account.code.is_empty() {
|
||||
return Err(eyre!(
|
||||
"Account {address} bytecode diff, expected {} got empty bytecode",
|
||||
test_account.code,
|
||||
))
|
||||
}
|
||||
|
||||
// get walker if present
|
||||
if let Some(storage) = storage.as_ref() {
|
||||
// iterate over storages
|
||||
for (JsonU256(key), JsonU256(value)) in test_account.storage.iter() {
|
||||
let our_value = storage
|
||||
.get(address)
|
||||
.ok_or_else(|| {
|
||||
eyre!(
|
||||
"Missing storage from test {storage:?} got {:?}",
|
||||
test_account.storage
|
||||
)
|
||||
})?
|
||||
.get(key)
|
||||
.ok_or_else(|| {
|
||||
eyre!(
|
||||
"Slot is missing from table {storage:?} got:{:?}",
|
||||
test_account.storage
|
||||
)
|
||||
})?;
|
||||
if value != our_value {
|
||||
return Err(eyre!(
|
||||
"Storage diff we got {address}: {storage:?} but expect: {:?}",
|
||||
test_account.storage
|
||||
))
|
||||
}
|
||||
}
|
||||
} else if !test_account.storage.is_empty() {
|
||||
return Err(eyre!(
|
||||
"Walker is not present, but storage is not empty.{:?}",
|
||||
test_account.storage
|
||||
))
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})??,
|
||||
None => debug!(target: "reth::cli", "No post-state"),
|
||||
}
|
||||
}
|
||||
Ok(TestOutcome::Passed)
|
||||
}
|
||||
1
testing/ef-tests/.gitignore
vendored
Normal file
1
testing/ef-tests/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
ethereum-tests
|
||||
25
testing/ef-tests/Cargo.toml
Normal file
25
testing/ef-tests/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "ef-tests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/paradigmxyz/reth"
|
||||
readme = "README.md"
|
||||
description = "Staged syncing primitives used in reth."
|
||||
|
||||
[features]
|
||||
ef-tests = []
|
||||
|
||||
[dependencies]
|
||||
reth-primitives = { path = "../../crates/primitives" }
|
||||
reth-db = { path = "../../crates/storage/db", features = ["mdbx", "test-utils"] }
|
||||
reth-provider = { path = "../../crates/storage/provider" }
|
||||
reth-stages = { path = "../../crates/stages" }
|
||||
reth-rlp = { path = "../../crates/rlp" }
|
||||
reth-interfaces = { path = "../../crates/interfaces" }
|
||||
reth-revm = { path = "../../crates/revm" }
|
||||
tokio = "1.28.1"
|
||||
walkdir = "2.3.3"
|
||||
serde = "1.0.163"
|
||||
serde_json = "1.0.96"
|
||||
thiserror = "1.0.40"
|
||||
16
testing/ef-tests/src/assert.rs
Normal file
16
testing/ef-tests/src/assert.rs
Normal file
@ -0,0 +1,16 @@
|
||||
//! Various assertion helpers.
|
||||
|
||||
use crate::Error;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// A helper like `assert_eq!` that instead returns `Err(Error::Assertion)` on failure.
|
||||
pub fn assert_equal<T>(left: T, right: T, msg: &str) -> Result<(), Error>
|
||||
where
|
||||
T: Eq + Debug,
|
||||
{
|
||||
if left != right {
|
||||
return Err(Error::Assertion(format!("{msg}. Left {:?}, right {:?}", left, right)))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
39
testing/ef-tests/src/case.rs
Normal file
39
testing/ef-tests/src/case.rs
Normal file
@ -0,0 +1,39 @@
|
||||
//! Test case definitions
|
||||
|
||||
use crate::result::{CaseResult, Error};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// A single test case, capable of loading a JSON description of itself and running it.
|
||||
///
|
||||
/// See <https://ethereum-tests.readthedocs.io/> for test specs.
|
||||
pub trait Case: Debug + Sync + Sized {
|
||||
/// A description of the test.
|
||||
fn description(&self) -> String {
|
||||
"no description".to_string()
|
||||
}
|
||||
|
||||
/// Load the test from the given file path.
|
||||
///
|
||||
/// The file can be assumed to be a valid EF test case as described on <https://ethereum-tests.readthedocs.io/>.
|
||||
fn load(path: &Path) -> Result<Self, Error>;
|
||||
|
||||
/// Run the test.
|
||||
fn run(&self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// A container for multiple test cases.
|
||||
#[derive(Debug)]
|
||||
pub struct Cases<T> {
|
||||
/// The contained test cases and the path to each test.
|
||||
pub test_cases: Vec<(PathBuf, T)>,
|
||||
}
|
||||
|
||||
impl<T: Case> Cases<T> {
|
||||
/// Run the contained test cases.
|
||||
pub fn run(&self) -> Vec<CaseResult> {
|
||||
self.test_cases.iter().map(|(path, case)| CaseResult::new(path, case, case.run())).collect()
|
||||
}
|
||||
}
|
||||
190
testing/ef-tests/src/cases/blockchain_test.rs
Normal file
190
testing/ef-tests/src/cases/blockchain_test.rs
Normal file
@ -0,0 +1,190 @@
|
||||
//! Test runners for `BlockchainTests` in <https://github.com/ethereum/tests>
|
||||
|
||||
use crate::{
|
||||
models::{BlockchainTest, ForkSpec, RootOrState},
|
||||
Case, Error, Suite,
|
||||
};
|
||||
use reth_db::mdbx::test_utils::create_test_rw_db;
|
||||
use reth_primitives::{BlockBody, SealedBlock, StageCheckpoint};
|
||||
use reth_provider::Transaction;
|
||||
use reth_stages::{stages::ExecutionStage, ExecInput, Stage, StageId};
|
||||
use std::{collections::BTreeMap, ffi::OsStr, fs, ops::Deref, path::Path, sync::Arc};
|
||||
|
||||
/// A handler for the blockchain test suite.
|
||||
#[derive(Debug)]
|
||||
pub struct BlockchainTests {
|
||||
suite: String,
|
||||
}
|
||||
|
||||
impl BlockchainTests {
|
||||
/// Create a new handler for a subset of the blockchain test suite.
|
||||
pub fn new(suite: String) -> Self {
|
||||
Self { suite }
|
||||
}
|
||||
}
|
||||
|
||||
impl Suite for BlockchainTests {
|
||||
type Case = BlockchainTestCase;
|
||||
|
||||
fn suite_name(&self) -> String {
|
||||
format!("BlockchainTests/{}", self.suite)
|
||||
}
|
||||
}
|
||||
|
||||
/// An Ethereum blockchain test.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct BlockchainTestCase {
|
||||
tests: BTreeMap<String, BlockchainTest>,
|
||||
skip: bool,
|
||||
}
|
||||
|
||||
impl Case for BlockchainTestCase {
|
||||
fn load(path: &Path) -> Result<Self, Error> {
|
||||
Ok(BlockchainTestCase {
|
||||
tests: fs::read_to_string(path)
|
||||
.map_err(|e| Error::Io { path: path.into(), error: e.to_string() })
|
||||
.and_then(|s| {
|
||||
serde_json::from_str(&s).map_err(|e| Error::CouldNotDeserialize {
|
||||
path: path.into(),
|
||||
error: e.to_string(),
|
||||
})
|
||||
})?,
|
||||
skip: should_skip(path),
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Clean up
|
||||
fn run(&self) -> Result<(), Error> {
|
||||
if self.skip {
|
||||
return Err(Error::Skipped)
|
||||
}
|
||||
|
||||
for case in self.tests.values() {
|
||||
if matches!(
|
||||
case.network,
|
||||
ForkSpec::ByzantiumToConstantinopleAt5 |
|
||||
ForkSpec::Constantinople |
|
||||
ForkSpec::ConstantinopleFix |
|
||||
ForkSpec::MergeEOF |
|
||||
ForkSpec::MergeMeterInitCode |
|
||||
ForkSpec::MergePush0 |
|
||||
ForkSpec::Unknown
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create the database
|
||||
let db = create_test_rw_db();
|
||||
let mut transaction = Transaction::new(db.as_ref())?;
|
||||
|
||||
// Insert test state
|
||||
reth_provider::insert_canonical_block(
|
||||
transaction.deref(),
|
||||
SealedBlock::new(case.genesis_block_header.clone().into(), BlockBody::default()),
|
||||
None,
|
||||
)?;
|
||||
case.pre.write_to_db(transaction.deref())?;
|
||||
|
||||
let mut last_block = None;
|
||||
for block in case.blocks.iter() {
|
||||
last_block = Some(block.write_to_db(transaction.deref())?);
|
||||
}
|
||||
|
||||
// Call execution stage
|
||||
{
|
||||
let mut stage = ExecutionStage::new_with_factory(reth_revm::Factory::new(
|
||||
Arc::new(case.network.clone().into()),
|
||||
));
|
||||
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.expect("Could not build tokio RT")
|
||||
.block_on(async {
|
||||
// ignore error
|
||||
let _ = stage
|
||||
.execute(
|
||||
&mut transaction,
|
||||
ExecInput {
|
||||
previous_stage: last_block
|
||||
.map(|b| (StageId("Dummy"), StageCheckpoint::new(b))),
|
||||
checkpoint: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
// Validate post state
|
||||
match &case.post_state {
|
||||
Some(RootOrState::Root(root)) => {
|
||||
// TODO: We should really check the state root here...
|
||||
println!("Post-state root: #{root:?}")
|
||||
}
|
||||
Some(RootOrState::State(state)) => {
|
||||
for (&address, account) in state.iter() {
|
||||
account.assert_db(address, transaction.deref())?;
|
||||
}
|
||||
}
|
||||
None => println!("No post-state"),
|
||||
}
|
||||
|
||||
transaction.close();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests are test edge cases that are not possible to happen on mainnet, so we are skipping them.
|
||||
pub fn should_skip(path: &Path) -> bool {
|
||||
// funky test with `bigint 0x00` value in json :) not possible to happen on mainnet and require
|
||||
// custom json parser. https://github.com/ethereum/tests/issues/971
|
||||
if path.file_name() == Some(OsStr::new("ValueOverflow.json")) {
|
||||
return true
|
||||
}
|
||||
// txbyte is of type 02 and we dont parse tx bytes for this test to fail.
|
||||
if path.file_name() == Some(OsStr::new("typeTwoBerlin.json")) {
|
||||
return true
|
||||
}
|
||||
// Test checks if nonce overflows. We are handling this correctly but we are not parsing
|
||||
// exception in testsuite There are more nonce overflow tests that are in internal
|
||||
// call/create, and those tests are passing and are enabled.
|
||||
if path.file_name() == Some(OsStr::new("CreateTransactionHighNonce.json")) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Test check if gas price overflows, we handle this correctly but does not match tests specific
|
||||
// exception.
|
||||
if path.file_name() == Some(OsStr::new("HighGasPrice.json")) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip test where basefee/accesslist/difficulty is present but it shouldn't be supported in
|
||||
// London/Berlin/TheMerge. https://github.com/ethereum/tests/blob/5b7e1ab3ffaf026d99d20b17bb30f533a2c80c8b/GeneralStateTests/stExample/eip1559.json#L130
|
||||
// It is expected to not execute these tests.
|
||||
if path.file_name() == Some(OsStr::new("accessListExample.json")) ||
|
||||
path.file_name() == Some(OsStr::new("basefeeExample.json")) ||
|
||||
path.file_name() == Some(OsStr::new("eip1559.json")) ||
|
||||
path.file_name() == Some(OsStr::new("mergeTest.json"))
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
// These tests are passing, but they take a lot of time to execute so we are going to skip them.
|
||||
if path.file_name() == Some(OsStr::new("loopExp.json")) ||
|
||||
path.file_name() == Some(OsStr::new("Call50000_sha256.json")) ||
|
||||
path.file_name() == Some(OsStr::new("static_Call50000_sha256.json")) ||
|
||||
path.file_name() == Some(OsStr::new("loopMul.json")) ||
|
||||
path.file_name() == Some(OsStr::new("CALLBlake2f_MaxRounds.json")) ||
|
||||
path.file_name() == Some(OsStr::new("shiftCombinations.json"))
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
// Ignore outdated EOF tests that haven't been updated for Cancun yet.
|
||||
let eof_path = Path::new("EIPTests").join("stEOF");
|
||||
if path.to_string_lossy().contains(&*eof_path.to_string_lossy()) {
|
||||
return true
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
3
testing/ef-tests/src/cases/mod.rs
Normal file
3
testing/ef-tests/src/cases/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
//! Specific test case handler implementations.
|
||||
|
||||
pub mod blockchain_test;
|
||||
20
testing/ef-tests/src/lib.rs
Normal file
20
testing/ef-tests/src/lib.rs
Normal file
@ -0,0 +1,20 @@
|
||||
#![warn(missing_debug_implementations, missing_docs, unreachable_pub)]
|
||||
#![deny(unused_must_use, rust_2018_idioms)]
|
||||
#![doc(test(
|
||||
no_crate_inject,
|
||||
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
|
||||
))]
|
||||
|
||||
//! Abstractions and runners for EF tests.
|
||||
|
||||
pub mod case;
|
||||
pub mod result;
|
||||
pub mod suite;
|
||||
|
||||
pub mod assert;
|
||||
pub mod cases;
|
||||
pub mod models;
|
||||
|
||||
pub use case::{Case, Cases};
|
||||
pub use result::{CaseResult, Error};
|
||||
pub use suite::Suite;
|
||||
@ -1,18 +1,24 @@
|
||||
use reth_primitives::{
|
||||
Address, BigEndianHash, Bloom, Bytes, ChainSpec, ChainSpecBuilder, Header as RethHeader,
|
||||
JsonU256, SealedHeader, Withdrawal, H160, H256, H64,
|
||||
//! Shared models for <https://github.com/ethereum/tests>
|
||||
|
||||
use crate::{assert::assert_equal, Error};
|
||||
use reth_db::{
|
||||
cursor::DbDupCursorRO,
|
||||
tables,
|
||||
transaction::{DbTx, DbTxMut},
|
||||
};
|
||||
use reth_primitives::{
|
||||
keccak256, Account as RethAccount, Address, BigEndianHash, BlockNumber, Bloom, Bytecode, Bytes,
|
||||
ChainSpec, ChainSpecBuilder, Header as RethHeader, JsonU256, SealedBlock, SealedHeader,
|
||||
StorageEntry, Withdrawal, H160, H256, H64, U256,
|
||||
};
|
||||
use reth_rlp::Decodable;
|
||||
use serde::{self, Deserialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::{collections::BTreeMap, ops::Deref};
|
||||
|
||||
/// An Ethereum blockchain test.
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
pub struct Test(pub BTreeMap<String, BlockchainTestData>);
|
||||
|
||||
/// Ethereum test data.
|
||||
/// The definition of a blockchain test.
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlockchainTestData {
|
||||
pub struct BlockchainTest {
|
||||
/// Genesis block header.
|
||||
pub genesis_block_header: Header,
|
||||
/// RLP encoded genesis block.
|
||||
@ -34,7 +40,7 @@ pub struct BlockchainTestData {
|
||||
}
|
||||
|
||||
/// A block header in an Ethereum blockchain test.
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Header {
|
||||
/// Bloom filter.
|
||||
@ -102,7 +108,6 @@ impl From<Header> for SealedHeader {
|
||||
|
||||
/// A block in an Ethereum blockchain test.
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Block {
|
||||
/// Block header.
|
||||
@ -119,7 +124,20 @@ pub struct Block {
|
||||
pub withdrawals: Option<Vec<Withdrawal>>,
|
||||
}
|
||||
|
||||
/// Transaction Sequence in block
|
||||
impl Block {
|
||||
/// Write the block to the database.
|
||||
pub fn write_to_db<'a, Tx>(&self, tx: &'a Tx) -> Result<BlockNumber, Error>
|
||||
where
|
||||
Tx: DbTx<'a> + DbTxMut<'a>,
|
||||
{
|
||||
let decoded = SealedBlock::decode(&mut self.rlp.as_ref())?;
|
||||
let block_number = decoded.number;
|
||||
reth_provider::insert_canonical_block(tx, decoded, None)?;
|
||||
Ok(block_number)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction sequence in block
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -131,7 +149,47 @@ pub struct TransactionSequence {
|
||||
|
||||
/// Ethereum blockchain test data state.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
|
||||
pub struct State(pub BTreeMap<Address, Account>);
|
||||
pub struct State(BTreeMap<Address, Account>);
|
||||
|
||||
impl State {
|
||||
/// Write the state to the database.
|
||||
pub fn write_to_db<'a, Tx>(&self, tx: &'a Tx) -> Result<(), Error>
|
||||
where
|
||||
Tx: DbTxMut<'a>,
|
||||
{
|
||||
for (&address, account) in self.0.iter() {
|
||||
let has_code = !account.code.is_empty();
|
||||
let code_hash = has_code.then(|| keccak256(&account.code));
|
||||
tx.put::<tables::PlainAccountState>(
|
||||
address,
|
||||
RethAccount {
|
||||
balance: account.balance.0,
|
||||
nonce: account.nonce.0.to::<u64>(),
|
||||
bytecode_hash: code_hash,
|
||||
},
|
||||
)?;
|
||||
if let Some(code_hash) = code_hash {
|
||||
tx.put::<tables::Bytecodes>(code_hash, Bytecode::new_raw(account.code.0.clone()))?;
|
||||
}
|
||||
account.storage.iter().try_for_each(|(k, v)| {
|
||||
tx.put::<tables::PlainStorageState>(
|
||||
address,
|
||||
StorageEntry { key: H256::from_slice(&k.0.to_be_bytes::<32>()), value: v.0 },
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for State {
|
||||
type Target = BTreeMap<Address, Account>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Merkle root hash or storage accounts.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||
@ -157,8 +215,62 @@ pub struct Account {
|
||||
pub storage: BTreeMap<JsonU256, JsonU256>,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// Check that the account matches what is in the database.
|
||||
///
|
||||
/// In case of a mismatch, `Err(Error::Assertion)` is returned.
|
||||
pub fn assert_db<'a, Tx>(&self, address: Address, tx: &'a Tx) -> Result<(), Error>
|
||||
where
|
||||
Tx: DbTx<'a>,
|
||||
{
|
||||
let account = tx.get::<tables::PlainAccountState>(address)?.ok_or_else(|| {
|
||||
Error::Assertion(format!("Account is missing ({address}) expected: {:?}", self))
|
||||
})?;
|
||||
|
||||
assert_equal(self.balance.into(), account.balance, "Balance does not match")?;
|
||||
assert_equal(self.nonce.0.to(), account.nonce, "Nonce does not match")?;
|
||||
|
||||
if let Some(bytecode_hash) = account.bytecode_hash {
|
||||
assert_equal(keccak256(&self.code), bytecode_hash, "Bytecode does not match")?;
|
||||
} else {
|
||||
assert_equal(
|
||||
self.code.is_empty(),
|
||||
true,
|
||||
"Expected empty bytecode, got bytecode in db.",
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut storage_cursor = tx.cursor_dup_read::<tables::PlainStorageState>()?;
|
||||
for (slot, value) in self.storage.iter() {
|
||||
if let Some(entry) =
|
||||
storage_cursor.seek_by_key_subkey(address, H256(slot.0.to_be_bytes()))?
|
||||
{
|
||||
if U256::from_be_bytes(entry.key.0) == slot.0 {
|
||||
assert_equal(
|
||||
value.0,
|
||||
entry.value,
|
||||
&format!("Storage for slot {:?} does not match", slot),
|
||||
)?;
|
||||
} else {
|
||||
return Err(Error::Assertion(format!(
|
||||
"Slot {:?} is missing from the database. Expected {:?}",
|
||||
slot, value
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
return Err(Error::Assertion(format!(
|
||||
"Slot {:?} is missing from the database. Expected {:?}",
|
||||
slot, value
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fork specification.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Ord, Deserialize)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Ord, Clone, Deserialize)]
|
||||
pub enum ForkSpec {
|
||||
/// Frontier
|
||||
Frontier,
|
||||
@ -310,129 +422,6 @@ mod test {
|
||||
use super::*;
|
||||
use serde_json;
|
||||
|
||||
#[test]
|
||||
fn blockchain_test_deserialize() {
|
||||
let test = r#"{
|
||||
"evmBytecode_d0g0v0_Berlin" : {
|
||||
"_info" : {
|
||||
"comment" : "",
|
||||
"filling-rpc-server" : "evm version 1.10.18-unstable-53304ff6-20220503",
|
||||
"filling-tool-version" : "retesteth-0.2.2-testinfo+commit.05e0b8ca.Linux.g++",
|
||||
"generatedTestHash" : "0951de8d9e6b2a08e57234f57ef719a17aee9d7e9d7e852e454a641028b791a9",
|
||||
"lllcversion" : "Version: 0.5.14-develop.2021.11.27+commit.401d5358.Linux.g++",
|
||||
"solidity" : "Version: 0.8.5+commit.a4f2e591.Linux.g++",
|
||||
"source" : "src/GeneralStateTestsFiller/stBugs/evmBytecodeFiller.json",
|
||||
"sourceHash" : "6ced7b43100305d1cc5aee48344c0eab6002940358e2c126279ef8444c2dea5a"
|
||||
},
|
||||
"blocks" : [
|
||||
{
|
||||
"blockHeader" : {
|
||||
"bloom" : "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"coinbase" : "0x1000000000000000000000000000000000000000",
|
||||
"difficulty" : "0x020000",
|
||||
"extraData" : "0x00",
|
||||
"gasLimit" : "0x54a60a4202e088",
|
||||
"gasUsed" : "0x01d4c0",
|
||||
"hash" : "0xb8152a06d2018ed9a1eb1eb6e1fe8c3478d8eae5b04d743bf4a1ec699510cfe5",
|
||||
"mixHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"nonce" : "0x0000000000000000",
|
||||
"number" : "0x01",
|
||||
"parentHash" : "0xb835c89a42605cfcc542381145b83c826caf10823b81af0f45091040a67e6601",
|
||||
"receiptTrie" : "0x0ef77336cf7bfbd2c500dcefe7b48d0ef7896d38f6373fbeb301ea4dac3746a7",
|
||||
"stateRoot" : "0x27bf1aca92967ecd83e11c52887203bbdcab73a27fe07e814cf749fa50483a53",
|
||||
"timestamp" : "0x03e8",
|
||||
"transactionsTrie" : "0x9008a2d4af552fea9b45675cd2af6d4117303b57da25b28438ccd1f6bad6828d",
|
||||
"uncleHash" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"
|
||||
},
|
||||
"rlp" : "0xf90264f901fca0b835c89a42605cfcc542381145b83c826caf10823b81af0f45091040a67e6601a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347941000000000000000000000000000000000000000a027bf1aca92967ecd83e11c52887203bbdcab73a27fe07e814cf749fa50483a53a09008a2d4af552fea9b45675cd2af6d4117303b57da25b28438ccd1f6bad6828da00ef77336cf7bfbd2c500dcefe7b48d0ef7896d38f6373fbeb301ea4dac3746a7b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018754a60a4202e0888301d4c08203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f862f860800a8301d4c094b94f5374fce5edbc8e2a8697c15331677e6ebf0b80801ca0f3b41c283c02ed98318dc9cac3f0ddc3de2f2f7853a03299a46f22a7c6726c3aa0396ccb5968a532ea070924408625d3e36d4db21bfbd0cb070ba9e1fe9dba58abc0",
|
||||
"transactions" : [
|
||||
{
|
||||
"data" : "0x",
|
||||
"gasLimit" : "0x01d4c0",
|
||||
"gasPrice" : "0x0a",
|
||||
"nonce" : "0x00",
|
||||
"r" : "0xf3b41c283c02ed98318dc9cac3f0ddc3de2f2f7853a03299a46f22a7c6726c3a",
|
||||
"s" : "0x396ccb5968a532ea070924408625d3e36d4db21bfbd0cb070ba9e1fe9dba58ab",
|
||||
"sender" : "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||
"to" : "0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||
"v" : "0x1c",
|
||||
"value" : "0x00"
|
||||
}
|
||||
],
|
||||
"uncleHeaders" : [
|
||||
],
|
||||
"withdrawals" : [
|
||||
]
|
||||
}
|
||||
],
|
||||
"genesisBlockHeader" : {
|
||||
"bloom" : "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"coinbase" : "0x1000000000000000000000000000000000000000",
|
||||
"difficulty" : "0x020000",
|
||||
"extraData" : "0x00",
|
||||
"gasLimit" : "0x54a60a4202e088",
|
||||
"gasUsed" : "0x00",
|
||||
"hash" : "0xb835c89a42605cfcc542381145b83c826caf10823b81af0f45091040a67e6601",
|
||||
"mixHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"nonce" : "0x0000000000000000",
|
||||
"number" : "0x00",
|
||||
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"receiptTrie" : "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||
"stateRoot" : "0x642a369a4a9dbf57d83ba05413910a5dd2cff93858c68e9e8293a8fffeae8660",
|
||||
"timestamp" : "0x00",
|
||||
"transactionsTrie" : "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||
"uncleHash" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"
|
||||
},
|
||||
"genesisRLP" : "0xf901fcf901f7a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347941000000000000000000000000000000000000000a0642a369a4a9dbf57d83ba05413910a5dd2cff93858c68e9e8293a8fffeae8660a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000808754a60a4202e088808000a00000000000000000000000000000000000000000000000000000000000000000880000000000000000c0c0",
|
||||
"lastblockhash" : "0xb8152a06d2018ed9a1eb1eb6e1fe8c3478d8eae5b04d743bf4a1ec699510cfe5",
|
||||
"network" : "Berlin",
|
||||
"postState" : {
|
||||
"0x1000000000000000000000000000000000000000" : {
|
||||
"balance" : "0x1bc16d674eda4f80",
|
||||
"code" : "0x",
|
||||
"nonce" : "0x00",
|
||||
"storage" : {
|
||||
}
|
||||
},
|
||||
"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : {
|
||||
"balance" : "0x38beec8feeb7d618",
|
||||
"code" : "0x",
|
||||
"nonce" : "0x01",
|
||||
"storage" : {
|
||||
}
|
||||
},
|
||||
"0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b" : {
|
||||
"balance" : "0x00",
|
||||
"code" : "0x67ffffffffffffffff600160006000fb",
|
||||
"nonce" : "0x3f",
|
||||
"storage" : {
|
||||
}
|
||||
}
|
||||
},
|
||||
"pre" : {
|
||||
"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : {
|
||||
"balance" : "0x38beec8feeca2598",
|
||||
"code" : "0x",
|
||||
"nonce" : "0x00",
|
||||
"storage" : {
|
||||
}
|
||||
},
|
||||
"0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b" : {
|
||||
"balance" : "0x00",
|
||||
"code" : "0x67ffffffffffffffff600160006000fb",
|
||||
"nonce" : "0x3f",
|
||||
"storage" : {
|
||||
}
|
||||
}
|
||||
},
|
||||
"sealEngine" : "NoProof"
|
||||
}
|
||||
}"#;
|
||||
|
||||
let res = serde_json::from_str::<Test>(test);
|
||||
assert!(res.is_ok(), "Failed to deserialize BlockchainTestData with error: {res:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_deserialize() {
|
||||
let test = r#"{
|
||||
123
testing/ef-tests/src/result.rs
Normal file
123
testing/ef-tests/src/result.rs
Normal file
@ -0,0 +1,123 @@
|
||||
//! Test results and errors
|
||||
|
||||
use crate::Case;
|
||||
use reth_db::DatabaseError;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Test errors
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// `Error::Skipped` should not be treated as a test failure.
|
||||
#[derive(Error, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// The test was skipped
|
||||
#[error("Test was skipped")]
|
||||
Skipped,
|
||||
/// An IO error occurred
|
||||
#[error("An error occurred interacting with the file system at {path}: {error}")]
|
||||
Io {
|
||||
/// The path to the file or directory
|
||||
path: PathBuf,
|
||||
/// The specific error
|
||||
error: String,
|
||||
},
|
||||
/// A deserialization error occurred
|
||||
#[error("An error occurred deserializing the test at {path}: {error}")]
|
||||
CouldNotDeserialize {
|
||||
/// The path to the file we wanted to deserialize
|
||||
path: PathBuf,
|
||||
/// The specific error
|
||||
error: String,
|
||||
},
|
||||
/// A database error occurred.
|
||||
#[error(transparent)]
|
||||
Database(#[from] DatabaseError),
|
||||
/// A test assertion failed.
|
||||
#[error("Test failed: {0}")]
|
||||
Assertion(String),
|
||||
/// An error internally in reth occurred.
|
||||
#[error("Test failed: {0}")]
|
||||
RethError(#[from] reth_interfaces::Error),
|
||||
/// An error occurred while decoding RLP.
|
||||
#[error("An error occurred deserializing RLP")]
|
||||
RlpDecodeError(#[from] reth_rlp::DecodeError),
|
||||
}
|
||||
|
||||
/// The result of running a test.
|
||||
#[derive(Debug)]
|
||||
pub struct CaseResult {
|
||||
/// A description of the test.
|
||||
pub desc: String,
|
||||
/// The full path to the test.
|
||||
pub path: PathBuf,
|
||||
/// The result of the test.
|
||||
pub result: Result<(), Error>,
|
||||
}
|
||||
|
||||
impl CaseResult {
|
||||
/// Create a new test result.
|
||||
pub fn new(path: &Path, case: &impl Case, result: Result<(), Error>) -> Self {
|
||||
CaseResult { desc: case.description(), path: path.into(), result }
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that all the given tests passed and print the results to stdout.
|
||||
pub(crate) fn assert_tests_pass(suite_name: &str, path: &Path, results: &[CaseResult]) {
|
||||
let (passed, failed, skipped) = categorize_results(results);
|
||||
|
||||
print_results(suite_name, path, &passed, &failed, &skipped);
|
||||
|
||||
if !failed.is_empty() {
|
||||
panic!("Some tests failed (see above)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Categorize test results into `(passed, failed, skipped)`.
|
||||
pub(crate) fn categorize_results(
|
||||
results: &[CaseResult],
|
||||
) -> (Vec<&CaseResult>, Vec<&CaseResult>, Vec<&CaseResult>) {
|
||||
let mut passed = Vec::new();
|
||||
let mut failed = Vec::new();
|
||||
let mut skipped = Vec::new();
|
||||
|
||||
for case in results {
|
||||
match case.result.as_ref().err() {
|
||||
Some(Error::Skipped) => skipped.push(case),
|
||||
Some(_) => failed.push(case),
|
||||
None => passed.push(case),
|
||||
}
|
||||
}
|
||||
|
||||
(passed, failed, skipped)
|
||||
}
|
||||
|
||||
/// Display the given test results to stdout.
|
||||
pub(crate) fn print_results(
|
||||
suite_name: &str,
|
||||
path: &Path,
|
||||
passed: &[&CaseResult],
|
||||
failed: &[&CaseResult],
|
||||
skipped: &[&CaseResult],
|
||||
) {
|
||||
println!("Suite: {suite_name} (at {})", path.display());
|
||||
println!(
|
||||
"Ran {} tests ({} passed, {} failed, {} skipped)",
|
||||
passed.len() + failed.len() + skipped.len(),
|
||||
passed.len(),
|
||||
failed.len(),
|
||||
skipped.len()
|
||||
);
|
||||
|
||||
for case in skipped {
|
||||
println!("[S] Case {} skipped", case.path.display());
|
||||
}
|
||||
|
||||
for case in failed {
|
||||
let error = case.result.clone().unwrap_err();
|
||||
|
||||
println!("[!] Case {} failed (description: {}): {}", case.path.display(), case.desc, error);
|
||||
}
|
||||
}
|
||||
57
testing/ef-tests/src/suite.rs
Normal file
57
testing/ef-tests/src/suite.rs
Normal file
@ -0,0 +1,57 @@
|
||||
//! Abstractions for groups of tests.
|
||||
|
||||
use crate::{
|
||||
case::{Case, Cases},
|
||||
result::assert_tests_pass,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
/// A collection of tests.
|
||||
pub trait Suite {
|
||||
/// The type of test cases in this suite.
|
||||
type Case: Case;
|
||||
|
||||
/// The name of the test suite used to locate the individual test cases.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// - `GeneralStateTests`
|
||||
/// - `BlockchainTests/InvalidBlocks`
|
||||
/// - `BlockchainTests/TransitionTests`
|
||||
fn suite_name(&self) -> String;
|
||||
|
||||
/// Load an run each contained test case.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This recursively finds every test description in the resulting path.
|
||||
fn run(&self) {
|
||||
let suite_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("ethereum-tests")
|
||||
.join(self.suite_name());
|
||||
|
||||
// todo: assert that the path exists
|
||||
let test_cases = find_all_files_with_extension(&suite_path, ".json")
|
||||
.into_iter()
|
||||
.map(|test_case_path| {
|
||||
let case = Self::Case::load(&test_case_path).expect("test case should load");
|
||||
(test_case_path, case)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let results = Cases { test_cases }.run();
|
||||
|
||||
assert_tests_pass(&self.suite_name(), &suite_path, &results);
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively find all files with a given extension.
|
||||
fn find_all_files_with_extension(path: &Path, extension: &str) -> Vec<PathBuf> {
|
||||
WalkDir::new(path)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| e.file_name().to_string_lossy().ends_with(extension))
|
||||
.map(DirEntry::into_path)
|
||||
.collect::<_>()
|
||||
}
|
||||
79
testing/ef-tests/tests/tests.rs
Normal file
79
testing/ef-tests/tests/tests.rs
Normal file
@ -0,0 +1,79 @@
|
||||
#![cfg(feature = "ef-tests")]
|
||||
|
||||
use ef_tests::{cases::blockchain_test::BlockchainTests, suite::Suite};
|
||||
|
||||
macro_rules! general_state_test {
|
||||
($test_name:ident, $dir:ident) => {
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
BlockchainTests::new(format!("GeneralStateTests/{}", stringify!($dir))).run();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod general_state_tests {
|
||||
use super::*;
|
||||
|
||||
general_state_test!(shanghai, Shanghai);
|
||||
general_state_test!(st_args_zero_one_balance, stArgsZeroOneBalance);
|
||||
general_state_test!(st_attack, stAttackTest);
|
||||
general_state_test!(st_bad_opcode, stBadOpcode);
|
||||
general_state_test!(st_bugs, stBugs);
|
||||
general_state_test!(st_call_codes, stCallCodes);
|
||||
general_state_test!(st_call_create_call_code, stCallCreateCallCodeTest);
|
||||
general_state_test!(
|
||||
st_call_delegate_codes_call_code_homestead,
|
||||
stCallDelegateCodesCallCodeHomestead
|
||||
);
|
||||
general_state_test!(st_call_delegate_codes_homestead, stCallDelegateCodesHomestead);
|
||||
general_state_test!(st_chain_id, stChainId);
|
||||
general_state_test!(st_code_copy_test, stCodeCopyTest);
|
||||
general_state_test!(st_code_size_limit, stCodeSizeLimit);
|
||||
general_state_test!(st_create2, stCreate2);
|
||||
general_state_test!(st_create, stCreateTest);
|
||||
general_state_test!(st_delegate_call_test_homestead, stDelegatecallTestHomestead);
|
||||
general_state_test!(st_eip150_gas_prices, stEIP150singleCodeGasPrices);
|
||||
general_state_test!(st_eip150, stEIP150Specific);
|
||||
general_state_test!(st_eip158, stEIP158Specific);
|
||||
general_state_test!(st_eip1559, stEIP1559);
|
||||
general_state_test!(st_eip2930, stEIP2930);
|
||||
general_state_test!(st_eip3607, stEIP3607);
|
||||
general_state_test!(st_example, stExample);
|
||||
general_state_test!(st_ext_codehash, stExtCodeHash);
|
||||
general_state_test!(st_homestead, stHomesteadSpecific);
|
||||
general_state_test!(st_init_code, stInitCodeTest);
|
||||
general_state_test!(st_log, stLogTests);
|
||||
general_state_test!(st_mem_expanding_eip150_calls, stMemExpandingEIP150Calls);
|
||||
general_state_test!(st_memory_stress, stMemoryStressTest);
|
||||
general_state_test!(st_memory, stMemoryTest);
|
||||
general_state_test!(st_non_zero_calls, stNonZeroCallsTest);
|
||||
general_state_test!(st_precompiles, stPreCompiledContracts);
|
||||
general_state_test!(st_precompiles2, stPreCompiledContracts2);
|
||||
general_state_test!(st_quadratic_complexity, stQuadraticComplexityTest);
|
||||
general_state_test!(st_random, stRandom);
|
||||
general_state_test!(st_random2, stRandom2);
|
||||
general_state_test!(st_recursive_create, stRecursiveCreate);
|
||||
general_state_test!(st_refund, stRefundTest);
|
||||
general_state_test!(st_return, stReturnDataTest);
|
||||
general_state_test!(st_revert, stRevertTest);
|
||||
general_state_test!(st_self_balance, stSelfBalance);
|
||||
general_state_test!(st_shift, stShift);
|
||||
general_state_test!(st_sload, stSLoadTest);
|
||||
general_state_test!(st_solidity, stSolidityTest);
|
||||
general_state_test!(st_special, stSpecialTest);
|
||||
general_state_test!(st_sstore, stSStoreTest);
|
||||
general_state_test!(st_stack, stStackTests);
|
||||
general_state_test!(st_static_call, stStaticCall);
|
||||
general_state_test!(st_static_flag, stStaticFlagEnabled);
|
||||
general_state_test!(st_system_operations, stSystemOperationsTest);
|
||||
general_state_test!(st_time_consuming, stTimeConsuming);
|
||||
general_state_test!(st_transaction, stTransactionTest);
|
||||
general_state_test!(st_wallet, stWalletTest);
|
||||
general_state_test!(st_zero_calls_revert, stZeroCallsRevert);
|
||||
general_state_test!(st_zero_calls, stZeroCallsTest);
|
||||
general_state_test!(st_zero_knowledge, stZeroKnowledge);
|
||||
general_state_test!(st_zero_knowledge2, stZeroKnowledge2);
|
||||
general_state_test!(vm_tests, VMTests);
|
||||
}
|
||||
|
||||
// TODO: Add ValidBlocks and InvalidBlocks tests
|
||||
Reference in New Issue
Block a user