//! Test runners for `BlockchainTests` in use crate::{ models::{BlockchainTest, ForkSpec}, Case, Error, Suite, }; use alloy_rlp::Decodable; use reth_db::test_utils::create_test_rw_db; use reth_primitives::{BlockBody, SealedBlock}; use reth_provider::{BlockWriter, HashingWriter, ProviderFactory}; 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 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(BlockchainTestCase { 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), }) } // 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 factory = ProviderFactory::new(db.as_ref(), Arc::new(case.network.clone().into())); let provider = factory.provider_rw().unwrap(); // Insert test state provider.insert_block( SealedBlock::new(case.genesis_block_header.clone().into(), BlockBody::default()), None, None, )?; case.pre.write_to_db(provider.tx_ref())?; let mut last_block = None; for block in case.blocks.iter() { let decoded = SealedBlock::decode(&mut block.rlp.as_ref())?; provider.insert_block(decoded.clone(), None, None)?; last_block = Some(decoded); } // Call execution stage { let mut stage = ExecutionStage::new_with_factory(reth_revm::Factory::new( Arc::new(case.network.clone().into()), )); let target = last_block.as_ref().map(|b| b.number); tokio::runtime::Builder::new_current_thread() .build() .expect("Could not build tokio RT") .block_on(async { // ignore error let _ = stage.execute(&provider, ExecInput { target, checkpoint: None }).await; }); } // Validate post state if let Some(state) = &case.post_state { for (&address, account) in state.iter() { account.assert_db(address, provider.tx_ref())?; } } else if let Some(expected_state_root) = case.post_state_hash { // `insert_hashes` will insert hashed data, compute the state root and match it to // expected internally let last_block = last_block.unwrap_or_default(); provider.insert_hashes( 0..=last_block.number, last_block.hash, expected_state_root, )?; } else { return Err(Error::MissingPostState) } // Drop provider without committing to the database. drop(provider); } 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" // txbyte is of type 02 and we dont 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" // 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) }