refactor: move ef tests to own testing crate (#2847)

This commit is contained in:
Bjerg
2023-05-26 04:02:08 +02:00
committed by GitHub
parent a441c136e3
commit cb829be089
18 changed files with 715 additions and 521 deletions

1
testing/ef-tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
ethereum-tests

View 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"

View 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(())
}

View 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()
}
}

View 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
}

View File

@ -0,0 +1,3 @@
//! Specific test case handler implementations.
pub mod blockchain_test;

View 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;

View 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:?}");
}
}

View 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);
}
}

View 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::<_>()
}

View 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