diff --git a/crates/vm/levm/Cargo.toml b/crates/vm/levm/Cargo.toml index d41d028e4..a67fee143 100644 --- a/crates/vm/levm/Cargo.toml +++ b/crates/vm/levm/Cargo.toml @@ -13,6 +13,7 @@ serde_json = { version = "1.0.117" } walkdir = "2.5.0" ethereum_rust-rlp.workspace = true keccak-hash = "0.10.0" +ethereum_rust-core.workspace = true [dev-dependencies] hex = "0.4.3" diff --git a/crates/vm/levm/README.md b/crates/vm/levm/README.md index e589c125c..a769eb058 100644 --- a/crates/vm/levm/README.md +++ b/crates/vm/levm/README.md @@ -31,6 +31,11 @@ Features: [CallFrame](./docs/callframe.md) ### Testing + +#### Unit Testing To run the project's tests, do `make test`. +#### EF Tests +To run the EF Tests first download them from [here](https://github.com/ethereum/tests/tree/develop/GeneralStateTests). Then, inside the `tests` folder, create another folder named `ef_testcases` and include all the downloaded folders inside it. + Run `make help` to see available commands diff --git a/crates/vm/levm/src/errors.rs b/crates/vm/levm/src/errors.rs index c3f372276..0f31ec990 100644 --- a/crates/vm/levm/src/errors.rs +++ b/crates/vm/levm/src/errors.rs @@ -32,6 +32,7 @@ pub enum VMError { NonceOverflow, } +#[derive(Debug, Clone)] pub enum OpcodeSuccess { Continue, Result(ResultReason), diff --git a/crates/vm/levm/src/opcode_handlers/system.rs b/crates/vm/levm/src/opcode_handlers/system.rs index 2f7a25fa6..de3734d07 100644 --- a/crates/vm/levm/src/opcode_handlers/system.rs +++ b/crates/vm/levm/src/opcode_handlers/system.rs @@ -1,6 +1,6 @@ use crate::{ constants::{call_opcode, SUCCESS_FOR_RETURN}, - errors::ResultReason, + errors::ResultReason, vm::word_to_address, }; use super::*; @@ -15,7 +15,7 @@ impl VM { current_call_frame: &mut CallFrame, ) -> Result { let gas = current_call_frame.stack.pop()?; - let code_address = Address::from_low_u64_be(current_call_frame.stack.pop()?.low_u64()); + let code_address = word_to_address(current_call_frame.stack.pop()?); let value = current_call_frame.stack.pop()?; let args_offset = current_call_frame .stack @@ -95,7 +95,7 @@ impl VM { current_call_frame: &mut CallFrame, ) -> Result { let gas = current_call_frame.stack.pop()?; - let code_address = Address::from_low_u64_be(current_call_frame.stack.pop()?.low_u64()); + let code_address = word_to_address(current_call_frame.stack.pop()?); let value = current_call_frame.stack.pop()?; let args_offset = current_call_frame.stack.pop()?.try_into().unwrap(); let args_size = current_call_frame.stack.pop()?.try_into().unwrap(); @@ -158,7 +158,7 @@ impl VM { current_call_frame: &mut CallFrame, ) -> Result { let gas = current_call_frame.stack.pop()?; - let code_address = Address::from_low_u64_be(current_call_frame.stack.pop()?.low_u64()); + let code_address = word_to_address(current_call_frame.stack.pop()?); let args_offset = current_call_frame.stack.pop()?.try_into().unwrap(); let args_size = current_call_frame.stack.pop()?.try_into().unwrap(); let ret_offset = current_call_frame.stack.pop()?.try_into().unwrap(); @@ -192,7 +192,7 @@ impl VM { current_call_frame: &mut CallFrame, ) -> Result { let gas = current_call_frame.stack.pop()?; - let code_address = Address::from_low_u64_be(current_call_frame.stack.pop()?.low_u64()); + let code_address = word_to_address(current_call_frame.stack.pop()?); let args_offset = current_call_frame.stack.pop()?.try_into().unwrap(); let args_size = current_call_frame.stack.pop()?.try_into().unwrap(); let ret_offset = current_call_frame.stack.pop()?.try_into().unwrap(); diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs index 952e1ba5a..bf0ece846 100644 --- a/crates/vm/levm/src/vm.rs +++ b/crates/vm/levm/src/vm.rs @@ -9,13 +9,14 @@ use ethereum_rust_rlp; use ethereum_rust_rlp::encode::RLPEncode; use ethereum_types::H160; use keccak_hash::keccak; +use serde::{Deserialize, Serialize}; use sha3::{Digest, Keccak256}; use std::{ collections::{HashMap, HashSet}, str::FromStr, }; -#[derive(Clone, Default, Debug, PartialEq, Eq)] +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Account { pub address: Address, pub balance: U256, @@ -24,7 +25,7 @@ pub struct Account { pub nonce: u64, } -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct StorageSlot { pub original_value: U256, pub current_value: U256, @@ -154,7 +155,7 @@ impl Db { #[derive(Debug, Clone, Default)] // TODO: https://github.com/lambdaclass/ethereum_rust/issues/604 pub struct Substate { - pub warm_addresses: HashSet
, + pub warm_addresses: HashSet
, // Should be a hashmap } #[derive(Debug, Default, Clone)] @@ -366,11 +367,10 @@ impl VM { if contract_code.len() > MAX_CODE_SIZE { return Err(VMError::ContractOutputTooBig); } - // Supposing contract code has contents - if contract_code[0] == INVALID_CONTRACT_PREFIX { + + if !contract_code.is_empty() && contract_code[0] == INVALID_CONTRACT_PREFIX { return Err(VMError::InvalidInitialByte); } - // If the initialization code completes successfully, a final contract-creation cost is paid, // the code-deposit cost, c, proportional to the size of the created contract’s code let creation_cost = 200 * contract_code.len(); diff --git a/crates/vm/levm/tests/eftests.rs b/crates/vm/levm/tests/eftests.rs new file mode 100644 index 000000000..1299b7a6c --- /dev/null +++ b/crates/vm/levm/tests/eftests.rs @@ -0,0 +1,334 @@ +use std::{ + collections::HashMap, + fs::{self, read_dir}, + path::{Path, PathBuf}, + str::FromStr, +}; + +use bytes::Bytes; +use ethereum_rust_levm::{ + errors::VMError, + vm::{Account, Db, StorageSlot, VM}, +}; +use ethereum_types::{Address, H256, U256}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Env { + current_base_fee: Option, + current_coinbase: Address, + current_difficulty: U256, + current_excess_blob_gas: Option, + current_gas_limit: U256, + current_number: U256, + current_random: Option, + current_timestamp: U256, + previous_hash: Option, +} + +// Taken from cmd/ef_tests/types.rs +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] +pub struct ReadedAccount { + pub balance: U256, + #[serde(rename = "code", with = "ethereum_rust_core::serde_utils::bytes")] + pub code: Bytes, + pub nonce: U256, + pub storage: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Transaction { + #[serde(rename = "data", with = "ethereum_rust_core::serde_utils::bytes::vec")] + data: Vec, + gas_limit: Vec, + gas_price: Option, + nonce: U256, + secret_key: H256, + sender: Address, + to: TxDestination, + value: Vec, // Using serde_json::Value would not rise an error, but, works? + access_lists: Option>>>, + blob_versioned_hashes: Option>, + max_fee_per_blob_gas: Option, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AccesList { + address: Address, + storage_keys: Vec, // U256 or Address? +} + +/// To cover the case when 'to' field is an empty string +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub enum TxDestination { + Some(Address), + #[default] + None, +} + +impl Serialize for TxDestination { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + TxDestination::Some(address) => serializer.serialize_str(&format!("{:#x}", address)), + TxDestination::None => serializer.serialize_none(), + } + } +} + +impl<'de> Deserialize<'de> for TxDestination { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let str_option = Option::::deserialize(deserializer)?; + match str_option { + Some(str) if !str.is_empty() => Ok(TxDestination::Some( + Address::from_str(str.trim_start_matches("0x")).map_err(|_| { + serde::de::Error::custom(format!("Failed to deserialize hex value {str}")) + })?, + )), + _ => Ok(TxDestination::None), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct Index { + data: u16, // Maybe could be u64, but biggest value i've seen is 452 + gas: u16, + value: u16, +} + +#[derive(Debug, Serialize, Deserialize)] +struct TransactionResults { + /// define an index of the transaction in txs vector that has been used for this result + indexes: Index, + /// hash of the post state after transaction execution + hash: H256, + /// log hash of the transaction logs + logs: H256, + /// the transaction bytes of the generated transaction + #[serde(rename = "txbytes", with = "ethereum_rust_core::serde_utils::bytes")] + txbytes: Bytes, + /// For a transaction that is supposed to fail, the exception + expect_exception: Option, +} + +/// Contains the necessary elements to run a test +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TestArgs { + #[serde(default, rename = "_info")] + pub info: Option, + /// Contains the environment, the block just before the one that runs the VM or executes the transaction + env: Env, + /// Contains the state of the accounts before the transaction execution + pre: HashMap, + /// Contains the state of the environment and db after the transaction execution + post: HashMap>, + /// Contains the transaction to execute + transaction: Transaction, +} + +fn file_extension_is_json(path: &Path) -> bool { + path.extension().map(|ext| ext == "json").unwrap_or(false) +} +fn directory_contents(path: &PathBuf, contents: &mut Vec) { + let sub_paths: Vec = read_dir(path) + .unwrap() + .filter_map(|entry| match entry { + Ok(direntry) => Some(direntry.path()), + Err(err) => { + eprintln!("Error reading directory entry: {}", err); + None + } + }) + .collect(); + + for sub_path in &sub_paths { + if sub_path.is_dir() { + directory_contents(sub_path, contents); + } else if file_extension_is_json(sub_path) { + let file_content = fs::read_to_string(sub_path).unwrap(); + contents.push(file_content); + } + } +} + +/// Parses the content of the files into the TestCase struct +fn parse_files() -> Vec { + let paths: Vec = read_dir("tests/ef_testcases") + .unwrap() + .filter_map(|entry| match entry { + Ok(direntry) => Some(direntry.path()), + Err(err) => { + eprintln!("Error reading directory entry: {}", err); + None + } + }) + .collect(); + + let mut contents = Vec::new(); + + for path in paths { + if path.is_dir() { + directory_contents(&path, &mut contents); + } else { + let file_content = fs::read_to_string(path).unwrap(); + contents.push(file_content); + } + } + + contents +} + +/// Note: This behaviour is not the complex that should be to have it's own function +fn parse_contents(json_contents: Vec) -> Vec> { + json_contents + .into_iter() + .map(|json_content| { + //println!("{}", &json_content[..55]); + serde_json::from_str(&json_content).expect("Unable to parse JSON") + }) + .collect() +} + +fn init_environment(test_args: &TestArgs) -> Result { + // Be careful with clone (performance) + let accounts = test_args + .pre + .clone() + .into_iter() + .map(|(add, acc)| { + let storage: HashMap = acc + .storage + .into_iter() + .map(|(key, val)| { + let slot = StorageSlot { + original_value: val, + current_value: val, + is_cold: true, + }; + (key, slot) + }) + .collect(); + let new_acc = Account { + address: add, + balance: acc.balance, + bytecode: acc.code, + storage, + nonce: acc.nonce.as_u64(), + }; + (add, new_acc) + }) + .collect(); + + let mut db = Db { + accounts, + block_hashes: Default::default(), // Dont know where is set + }; + + let destination: Option
= match test_args.transaction.to { + TxDestination::Some(address) => Some(address), + TxDestination::None => None, + }; + // Note: here TxDestination should be matched to an Address Option (when creating is Null) + // but Vm does not support an option in address, so create is not supported currently + + VM::new( + destination, + test_args.transaction.sender, + test_args.transaction.value[0], // Maybe there's more than one value, see GeneralStateTests/stTransactionTest/NoSrcAccount.json + test_args.transaction.data[0].clone(), // There's more than one values in vector, see GeneralStateTests/Cancun/stEIP1153-transientStorage/transStorageOK.json + test_args.transaction.gas_limit[0], // Same as above comments + test_args.env.current_number, + test_args.env.current_coinbase, + test_args.env.current_timestamp, + Some(H256::zero()), + U256::one(), + test_args.env.current_base_fee.unwrap(), + test_args + .transaction + .gas_price + .unwrap_or(test_args.transaction.max_fee_per_gas.unwrap_or_default()), + &mut db, + Default::default(), // Dont know where is set + test_args.env.current_excess_blob_gas, + Default::default(), // Dont know where is set + test_args.transaction.secret_key, // Dont know where is set + None, + ) +} + +#[test] +fn ethereum_foundation_general_state_tests() { + // At this point Ethereum foundation tests should be already downloaded. + // The ones from https://github.com/ethereum/tests/tree/develop/GeneralStateTests + + let json_contents = parse_files(); + + let tests_cases: Vec> = parse_contents(json_contents); + + let tests_cases = &tests_cases[0..1]; + + for test_case in tests_cases { + //Maybe there are more than one test per hashmap, so should iterate each hashmap too + + for (test_name, test_args) in test_case { + // Initialize + println!("Parseando el test {:?}", test_name); + + match test_args.transaction.to { + TxDestination::Some(_) => { + let mut vm = + init_environment(test_args).expect("An error happened at init of test."); + + // Execute + vm.transact() + .expect("An error happened while executing the transaction."); + } + TxDestination::None => { + init_environment(test_args) + .expect("An error happened while executing the transaction."); + } + }; + + // Execute + //println!("Executing testcase {test_name}"); + //let _result = vm.transact(); + + // Verify + /* + Possible tests: + - Verify the result of the execution + - Verify the hash state + - Verify the result of the accounts individually (possibly require equal ) + - Check if gas costs have been applied to the sender + */ + + /* + if let Err(e) = result { + if e.into() == test_args.post.get("Cancun").unwrap().get(0).unwrap().expect_exception.unwrap() { + println!("El error es el esperado: {:?}", e); + } + } + */ + /* + // See if vm.call_frames.len() is equivalent to the amount of logs + for i in 0..vm.call_frames.len() { + assert_eq!(test_args.post.get("Cancun").unwrap().get(i).unwrap().log, from(vm.call_frames[0].logs[i].data)); + } + */ + } + } + + // unimplemented!(); +} diff --git a/crates/vm/levm/tests/lib.rs b/crates/vm/levm/tests/lib.rs index 15ab56057..7fc8e13f8 100644 --- a/crates/vm/levm/tests/lib.rs +++ b/crates/vm/levm/tests/lib.rs @@ -1 +1,3 @@ pub mod tests; + +pub use ethereum_types::*;