//! Test runners for `BlockchainTests` in use crate::{ models::{BlockchainTest, ForkSpec}, Case, Error, Suite, }; use alloy_rlp::Decodable; use rayon::iter::{ParallelBridge, ParallelIterator}; use reth_primitives::{BlockBody, SealedBlock, StaticFileSegment}; use reth_provider::{ providers::StaticFileWriter, test_utils::create_test_provider_factory_with_chain_spec, HashingWriter, }; use reth_stages::{stages::ExecutionStage, ExecInput, Stage}; use std::{collections::BTreeMap, fs, 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 const 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, skip: bool, } impl Case for BlockchainTestCase { fn load(path: &Path) -> Result { Ok(Self { tests: { let s = fs::read_to_string(path) .map_err(|error| Error::Io { path: path.into(), error })?; serde_json::from_str(&s) .map_err(|error| Error::CouldNotDeserialize { path: path.into(), error })? }, skip: should_skip(path), }) } /// Runs the test cases for the Ethereum Forks test suite. /// /// # Errors /// Returns an error if the test is flagged for skipping or encounters issues during execution. fn run(&self) -> Result<(), Error> { // If the test is marked for skipping, return a Skipped error immediately. if self.skip { return Err(Error::Skipped) } // Iterate through test cases, filtering by the network type to exclude specific forks. self.tests .values() .filter(|case| { !matches!( case.network, ForkSpec::ByzantiumToConstantinopleAt5 | ForkSpec::Constantinople | ForkSpec::ConstantinopleFix | ForkSpec::MergeEOF | ForkSpec::MergeMeterInitCode | ForkSpec::MergePush0 | ForkSpec::Unknown ) }) .par_bridge() .try_for_each(|case| { // Create a new test database and initialize a provider for the test case. let provider = create_test_provider_factory_with_chain_spec(Arc::new( case.network.clone().into(), )) .provider_rw() .unwrap(); // Insert initial test state into the provider. provider.insert_historical_block( SealedBlock::new( case.genesis_block_header.clone().into(), BlockBody::default(), ) .try_seal_with_senders() .unwrap(), )?; case.pre.write_to_db(provider.tx_ref())?; // Initialize receipts static file with genesis { let mut receipts_writer = provider .static_file_provider() .latest_writer(StaticFileSegment::Receipts) .unwrap(); receipts_writer.increment_block(0).unwrap(); receipts_writer.commit_without_sync_all().unwrap(); } // Decode and insert blocks, creating a chain of blocks for the test case. let last_block = case.blocks.iter().try_fold(None, |_, block| { let decoded = SealedBlock::decode(&mut block.rlp.as_ref())?; provider.insert_historical_block( decoded.clone().try_seal_with_senders().unwrap(), )?; Ok::, Error>(Some(decoded)) })?; provider .static_file_provider() .latest_writer(StaticFileSegment::Headers) .unwrap() .commit_without_sync_all() .unwrap(); // Execute the execution stage using the EVM processor factory for the test case // network. let _ = ExecutionStage::new_with_executor( reth_evm_ethereum::execute::EthExecutorProvider::ethereum(Arc::new( case.network.clone().into(), )), ) .execute( &provider, ExecInput { target: last_block.as_ref().map(|b| b.number), checkpoint: None }, ); // Validate the post-state for the test case. match (&case.post_state, &case.post_state_hash) { (Some(state), None) => { // Validate accounts in the state against the provider's database. for (&address, account) in state { account.assert_db(address, provider.tx_ref())?; } } (None, Some(expected_state_root)) => { // Insert state hashes into the provider based on the expected state root. let last_block = last_block.unwrap_or_default(); provider.insert_hashes( 0..=last_block.number, last_block.hash(), *expected_state_root, )?; } _ => return Err(Error::MissingPostState), } // Drop the provider without committing to the database. drop(provider); Ok(()) })?; Ok(()) } } /// Returns whether the test at the given path should be skipped. /// /// Some tests are edge cases that cannot happen on mainnet, while others are skipped for /// convenience (e.g. they take a long time to run) or are temporarily disabled. /// /// The reason should be documented in a comment above the file name(s). pub fn should_skip(path: &Path) -> bool { let path_str = path.to_str().expect("Path is not valid UTF-8"); let name = path.file_name().unwrap().to_str().unwrap(); matches!( name, // 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 | "ValueOverflow.json" | "ValueOverflowParis.json" // txbyte is of type 02 and we don't parse tx bytes for this test to fail. | "typeTwoBerlin.json" // 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. | "CreateTransactionHighNonce.json" // Test check if gas price overflows, we handle this correctly but does not match tests specific // exception. | "HighGasPrice.json" | "HighGasPriceParis.json" // 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. | "accessListExample.json" | "basefeeExample.json" | "eip1559.json" | "mergeTest.json" // These tests are passing, but they take a lot of time to execute so we are going to skip them. | "loopExp.json" | "Call50000_sha256.json" | "static_Call50000_sha256.json" | "loopMul.json" | "CALLBlake2f_MaxRounds.json" | "shiftCombinations.json" ) // Ignore outdated EOF tests that haven't been updated for Cancun yet. || path_contains(path_str, &["EIPTests", "stEOF"]) } /// `str::contains` but for a path. Takes into account the OS path separator (`/` or `\`). fn path_contains(path_str: &str, rhs: &[&str]) -> bool { let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR); path_str.contains(&rhs) }