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:
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;
|
||||
475
testing/ef-tests/src/models.rs
Normal file
475
testing/ef-tests/src/models.rs
Normal file
@ -0,0 +1,475 @@
|
||||
//! 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, ops::Deref};
|
||||
|
||||
/// The definition of a blockchain test.
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlockchainTest {
|
||||
/// Genesis block header.
|
||||
pub genesis_block_header: Header,
|
||||
/// RLP encoded genesis block.
|
||||
#[serde(rename = "genesisRLP")]
|
||||
pub genesis_rlp: Option<Bytes>,
|
||||
/// Block data.
|
||||
pub blocks: Vec<Block>,
|
||||
/// The expected post state.
|
||||
pub post_state: Option<RootOrState>,
|
||||
/// The test pre-state.
|
||||
pub pre: State,
|
||||
/// Hash of the best block.
|
||||
pub lastblockhash: H256,
|
||||
/// Network spec.
|
||||
pub network: ForkSpec,
|
||||
#[serde(default)]
|
||||
/// Engine spec.
|
||||
pub self_engine: SealEngine,
|
||||
}
|
||||
|
||||
/// A block header in an Ethereum blockchain test.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Header {
|
||||
/// Bloom filter.
|
||||
pub bloom: Bloom,
|
||||
/// Coinbase.
|
||||
pub coinbase: Address,
|
||||
/// Difficulty.
|
||||
pub difficulty: JsonU256,
|
||||
/// Extra data.
|
||||
pub extra_data: Bytes,
|
||||
/// Gas limit.
|
||||
pub gas_limit: JsonU256,
|
||||
/// Gas used.
|
||||
pub gas_used: JsonU256,
|
||||
/// Block Hash.
|
||||
pub hash: H256,
|
||||
/// Mix hash.
|
||||
pub mix_hash: H256,
|
||||
/// Seal nonce.
|
||||
pub nonce: H64,
|
||||
/// Block number.
|
||||
pub number: JsonU256,
|
||||
/// Parent hash.
|
||||
pub parent_hash: H256,
|
||||
/// Receipt trie.
|
||||
pub receipt_trie: H256,
|
||||
/// State root.
|
||||
pub state_root: H256,
|
||||
/// Timestamp.
|
||||
pub timestamp: JsonU256,
|
||||
/// Transactions trie.
|
||||
pub transactions_trie: H256,
|
||||
/// Uncle hash.
|
||||
pub uncle_hash: H256,
|
||||
/// Base fee per gas.
|
||||
pub base_fee_per_gas: Option<JsonU256>,
|
||||
/// Withdrawals root.
|
||||
pub withdrawals_root: Option<H256>,
|
||||
}
|
||||
|
||||
impl From<Header> for SealedHeader {
|
||||
fn from(value: Header) -> Self {
|
||||
let header = RethHeader {
|
||||
base_fee_per_gas: value.base_fee_per_gas.map(|v| v.0.to::<u64>()),
|
||||
beneficiary: value.coinbase,
|
||||
difficulty: value.difficulty.0,
|
||||
extra_data: value.extra_data,
|
||||
gas_limit: value.gas_limit.0.to::<u64>(),
|
||||
gas_used: value.gas_used.0.to::<u64>(),
|
||||
mix_hash: value.mix_hash,
|
||||
nonce: value.nonce.into_uint().as_u64(),
|
||||
number: value.number.0.to::<u64>(),
|
||||
timestamp: value.timestamp.0.to::<u64>(),
|
||||
transactions_root: value.transactions_trie,
|
||||
receipts_root: value.receipt_trie,
|
||||
ommers_hash: value.uncle_hash,
|
||||
state_root: value.state_root,
|
||||
parent_hash: value.parent_hash,
|
||||
logs_bloom: value.bloom,
|
||||
withdrawals_root: value.withdrawals_root,
|
||||
};
|
||||
header.seal(value.hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// A block in an Ethereum blockchain test.
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Block {
|
||||
/// Block header.
|
||||
pub block_header: Option<Header>,
|
||||
/// RLP encoded block bytes
|
||||
pub rlp: Bytes,
|
||||
/// Transactions
|
||||
pub transactions: Option<Vec<Transaction>>,
|
||||
/// Uncle/ommer headers
|
||||
pub uncle_headers: Option<Vec<Header>>,
|
||||
/// Transaction Sequence
|
||||
pub transaction_sequence: Option<Vec<TransactionSequence>>,
|
||||
/// Withdrawals
|
||||
pub withdrawals: Option<Vec<Withdrawal>>,
|
||||
}
|
||||
|
||||
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")]
|
||||
pub struct TransactionSequence {
|
||||
exception: String,
|
||||
raw_bytes: Bytes,
|
||||
valid: String,
|
||||
}
|
||||
|
||||
/// Ethereum blockchain test data state.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
|
||||
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)]
|
||||
#[serde(untagged)]
|
||||
pub enum RootOrState {
|
||||
/// If state is too big, only state root is present
|
||||
Root(H256),
|
||||
/// State
|
||||
State(BTreeMap<Address, Account>),
|
||||
}
|
||||
|
||||
/// An account.
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Account {
|
||||
/// Balance.
|
||||
pub balance: JsonU256,
|
||||
/// Code.
|
||||
pub code: Bytes,
|
||||
/// Nonce.
|
||||
pub nonce: JsonU256,
|
||||
/// Storage.
|
||||
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, Clone, Deserialize)]
|
||||
pub enum ForkSpec {
|
||||
/// Frontier
|
||||
Frontier,
|
||||
/// Frontier to Homestead
|
||||
FrontierToHomesteadAt5,
|
||||
/// Homestead
|
||||
Homestead,
|
||||
/// Homestead to Tangerine
|
||||
HomesteadToDaoAt5,
|
||||
/// Homestead to Tangerine
|
||||
HomesteadToEIP150At5,
|
||||
/// Tangerine
|
||||
EIP150,
|
||||
/// Spurious Dragon
|
||||
EIP158, // EIP-161: State trie clearing
|
||||
/// Spurious Dragon to Byzantium
|
||||
EIP158ToByzantiumAt5,
|
||||
/// Byzantium
|
||||
Byzantium,
|
||||
/// Byzantium to Constantinople
|
||||
ByzantiumToConstantinopleAt5, // SKIPPED
|
||||
/// Byzantium to Constantinople
|
||||
ByzantiumToConstantinopleFixAt5,
|
||||
/// Constantinople
|
||||
Constantinople, // SKIPPED
|
||||
/// Constantinople fix
|
||||
ConstantinopleFix,
|
||||
/// Istanbul
|
||||
Istanbul,
|
||||
/// Berlin
|
||||
Berlin,
|
||||
/// Berlin to London
|
||||
BerlinToLondonAt5,
|
||||
/// London
|
||||
London,
|
||||
/// Paris aka The Merge
|
||||
Merge,
|
||||
/// Shanghai
|
||||
Shanghai,
|
||||
/// Merge EOF test
|
||||
#[serde(alias = "Merge+3540+3670")]
|
||||
MergeEOF,
|
||||
/// After Merge Init Code test
|
||||
#[serde(alias = "Merge+3860")]
|
||||
MergeMeterInitCode,
|
||||
/// After Merge plus new PUSH0 opcode
|
||||
#[serde(alias = "Merge+3855")]
|
||||
MergePush0,
|
||||
/// Fork Spec which is unknown to us
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<ForkSpec> for ChainSpec {
|
||||
fn from(fork_spec: ForkSpec) -> Self {
|
||||
let spec_builder = ChainSpecBuilder::mainnet();
|
||||
|
||||
match fork_spec {
|
||||
ForkSpec::Frontier => spec_builder.frontier_activated(),
|
||||
ForkSpec::Homestead | ForkSpec::FrontierToHomesteadAt5 => {
|
||||
spec_builder.homestead_activated()
|
||||
}
|
||||
ForkSpec::EIP150 | ForkSpec::HomesteadToDaoAt5 | ForkSpec::HomesteadToEIP150At5 => {
|
||||
spec_builder.tangerine_whistle_activated()
|
||||
}
|
||||
ForkSpec::EIP158 => spec_builder.spurious_dragon_activated(),
|
||||
ForkSpec::Byzantium |
|
||||
ForkSpec::EIP158ToByzantiumAt5 |
|
||||
ForkSpec::ConstantinopleFix |
|
||||
ForkSpec::ByzantiumToConstantinopleFixAt5 => spec_builder.byzantium_activated(),
|
||||
ForkSpec::Istanbul => spec_builder.istanbul_activated(),
|
||||
ForkSpec::Berlin => spec_builder.berlin_activated(),
|
||||
ForkSpec::London | ForkSpec::BerlinToLondonAt5 => spec_builder.london_activated(),
|
||||
ForkSpec::Merge => spec_builder.paris_activated(),
|
||||
ForkSpec::MergeEOF => spec_builder.paris_activated(),
|
||||
ForkSpec::MergeMeterInitCode => spec_builder.paris_activated(),
|
||||
ForkSpec::MergePush0 => spec_builder.paris_activated(),
|
||||
ForkSpec::Shanghai => spec_builder.shanghai_activated(),
|
||||
ForkSpec::ByzantiumToConstantinopleAt5 | ForkSpec::Constantinople => {
|
||||
panic!("Overridden with PETERSBURG")
|
||||
}
|
||||
ForkSpec::Unknown => {
|
||||
panic!("Unknown fork");
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible seal engines.
|
||||
#[derive(Debug, PartialEq, Eq, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SealEngine {
|
||||
/// No consensus checks.
|
||||
#[default]
|
||||
NoProof,
|
||||
}
|
||||
|
||||
/// Ethereum blockchain test transaction data.
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Transaction {
|
||||
/// Transaction type
|
||||
#[serde(rename = "type")]
|
||||
pub transaction_type: Option<JsonU256>,
|
||||
/// Data.
|
||||
pub data: Bytes,
|
||||
/// Gas limit.
|
||||
pub gas_limit: JsonU256,
|
||||
/// Gas price.
|
||||
pub gas_price: Option<JsonU256>,
|
||||
/// Nonce.
|
||||
pub nonce: JsonU256,
|
||||
/// Signature r part.
|
||||
pub r: JsonU256,
|
||||
/// Signature s part.
|
||||
pub s: JsonU256,
|
||||
/// Parity bit.
|
||||
pub v: JsonU256,
|
||||
/// Transaction value.
|
||||
pub value: JsonU256,
|
||||
/// Chain ID.
|
||||
pub chain_id: Option<JsonU256>,
|
||||
/// Access list.
|
||||
pub access_list: Option<AccessList>,
|
||||
/// Max fee per gas.
|
||||
pub max_fee_per_gas: Option<JsonU256>,
|
||||
/// Max priority fee per gas
|
||||
pub max_priority_fee_per_gas: Option<JsonU256>,
|
||||
/// Transaction hash.
|
||||
pub hash: Option<H256>,
|
||||
}
|
||||
|
||||
/// Access list item
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccessListItem {
|
||||
/// Account address
|
||||
pub address: H160,
|
||||
/// Storage key.
|
||||
pub storage_keys: Vec<H256>,
|
||||
}
|
||||
|
||||
/// Access list.
|
||||
pub type AccessList = Vec<AccessListItem>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use serde_json;
|
||||
|
||||
#[test]
|
||||
fn header_deserialize() {
|
||||
let test = r#"{
|
||||
"baseFeePerGas" : "0x0a",
|
||||
"bloom" : "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"coinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba",
|
||||
"difficulty" : "0x020000",
|
||||
"extraData" : "0x00",
|
||||
"gasLimit" : "0x10000000000000",
|
||||
"gasUsed" : "0x10000000000000",
|
||||
"hash" : "0x7ebfee2a2c785fef181b8ffd92d4a48a0660ec000f465f309757e3f092d13882",
|
||||
"mixHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"nonce" : "0x0000000000000000",
|
||||
"number" : "0x01",
|
||||
"parentHash" : "0xa8f2eb2ea9dccbf725801eef5a31ce59bada431e888dfd5501677cc4365dc3be",
|
||||
"receiptTrie" : "0xbdd943f5c62ae0299324244a0f65524337ada9817e18e1764631cc1424f3a293",
|
||||
"stateRoot" : "0xc9c6306ee3e5acbaabe8e2fa28a10c12e27bad1d1aacc271665149f70519f8b0",
|
||||
"timestamp" : "0x03e8",
|
||||
"transactionsTrie" : "0xf5893b055ca05e4f14d1792745586a1376e218180bd56bd96b2b024e1dc78300",
|
||||
"uncleHash" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"
|
||||
}"#;
|
||||
let res = serde_json::from_str::<Header>(test);
|
||||
assert!(res.is_ok(), "Failed to deserialize Header with error: {res:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_deserialize() {
|
||||
let test = r#"[
|
||||
{
|
||||
"accessList" : [
|
||||
],
|
||||
"chainId" : "0x01",
|
||||
"data" : "0x693c61390000000000000000000000000000000000000000000000000000000000000000",
|
||||
"gasLimit" : "0x10000000000000",
|
||||
"maxFeePerGas" : "0x07d0",
|
||||
"maxPriorityFeePerGas" : "0x00",
|
||||
"nonce" : "0x01",
|
||||
"r" : "0x5fecc3972a35c9e341b41b0c269d9a7325e13269fb01c2f64cbce1046b3441c8",
|
||||
"s" : "0x7d4d0eda0e4ebd53c5d0b6fc35c600b317f8fa873b3963ab623ec9cec7d969bd",
|
||||
"sender" : "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||
"to" : "0xcccccccccccccccccccccccccccccccccccccccc",
|
||||
"type" : "0x02",
|
||||
"v" : "0x01",
|
||||
"value" : "0x00"
|
||||
}
|
||||
]"#;
|
||||
|
||||
let res = serde_json::from_str::<Vec<Transaction>>(test);
|
||||
assert!(res.is_ok(), "Failed to deserialize transaction with error: {res:?}");
|
||||
}
|
||||
}
|
||||
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