From 4520fa6bd131b42c60d3950ad036933a407084de Mon Sep 17 00:00:00 2001 From: Wei Tang Date: Fri, 10 Nov 2023 01:13:38 +0100 Subject: [PATCH] Reimplementation of jsontests in Apache-2 license (#209) * Basic state test decoding * Basic run structure * Init in-memory backend * Implement Backend and FullBackend for InMemoryBackend * Init cloned backend * Finish implementation of state root check * Fix cargo fmt --- Cargo.toml | 1 + interpreter/Cargo.toml | 2 +- interpreter/src/call_create.rs | 4 +- interpreter/src/error.rs | 18 ++ interpreter/src/eval/system.rs | 8 +- interpreter/src/lib.rs | 2 +- interpreter/src/runtime.rs | 39 +++-- interpreter/tests/usability.rs | 43 ++++- jsontests/Cargo.toml | 18 ++ jsontests/src/error.rs | 21 +++ jsontests/src/hash.rs | 94 ++++++++++ jsontests/src/main.rs | 38 ++++ jsontests/src/run.rs | 94 ++++++++++ jsontests/src/types.rs | 189 ++++++++++++++++++++ src/backend/in_memory.rs | 273 +++++++++++++++++++++++++++++ src/{backend.rs => backend/mod.rs} | 2 + src/lib.rs | 2 +- src/standard/invoker.rs | 88 ++++++---- 18 files changed, 876 insertions(+), 60 deletions(-) create mode 100644 jsontests/Cargo.toml create mode 100644 jsontests/src/error.rs create mode 100644 jsontests/src/hash.rs create mode 100644 jsontests/src/main.rs create mode 100644 jsontests/src/run.rs create mode 100644 jsontests/src/types.rs create mode 100644 src/backend/in_memory.rs rename src/{backend.rs => backend/mod.rs} (90%) diff --git a/Cargo.toml b/Cargo.toml index b26032d5..3a69966b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,4 +60,5 @@ with-serde = [ [workspace] members = [ "interpreter", + "jsontests", ] diff --git a/interpreter/Cargo.toml b/interpreter/Cargo.toml index 857eeb85..84ba097b 100644 --- a/interpreter/Cargo.toml +++ b/interpreter/Cargo.toml @@ -10,7 +10,7 @@ edition = "2018" [dependencies] log = { version = "0.4", optional = true } -primitive-types = { version = "0.12", default-features = false } +primitive-types = { version = "0.12", default-features = false, features = ["rlp"] } scale-codec = { package = "parity-scale-codec", version = "3.2", default-features = false, features = ["derive", "full"], optional = true } scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } diff --git a/interpreter/src/call_create.rs b/interpreter/src/call_create.rs index 9e844c0d..b053c46e 100644 --- a/interpreter/src/call_create.rs +++ b/interpreter/src/call_create.rs @@ -1,6 +1,6 @@ use crate::utils::{h256_to_u256, u256_to_usize}; use crate::{ - Context, ExitError, ExitException, ExitResult, Machine, Memory, Opcode, RuntimeFullBackend, + Context, ExitError, ExitException, ExitResult, Machine, Memory, Opcode, RuntimeBackend, RuntimeState, Transfer, }; use core::cmp::{max, min}; @@ -27,7 +27,7 @@ pub enum CreateScheme { } impl CreateScheme { - pub fn address(&self, handler: &H) -> H160 { + pub fn address(&self, handler: &H) -> H160 { match self { CreateScheme::Create2 { caller, diff --git a/interpreter/src/error.rs b/interpreter/src/error.rs index c4787e67..58e1baf0 100644 --- a/interpreter/src/error.rs +++ b/interpreter/src/error.rs @@ -52,6 +52,24 @@ impl From for ExitResult { } } +#[cfg(feature = "std")] +impl std::error::Error for ExitError { + fn description(&self) -> &str { + match self { + Self::Exception(_) => "EVM exit exception", + Self::Reverted => "EVM internal revert", + Self::Fatal(_) => "EVM fatal error", + } + } +} + +#[cfg(feature = "std")] +impl std::fmt::Display for ExitError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + /// Exit succeed reason. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg_attr( diff --git a/interpreter/src/eval/system.rs b/interpreter/src/eval/system.rs index 2bbb1ebe..6e183faa 100644 --- a/interpreter/src/eval/system.rs +++ b/interpreter/src/eval/system.rs @@ -1,5 +1,5 @@ use super::Control; -use crate::{ExitException, ExitFatal, ExitSucceed, Machine, RuntimeBackend, RuntimeState}; +use crate::{ExitException, ExitFatal, ExitSucceed, Log, Machine, RuntimeBackend, RuntimeState}; use alloc::vec::Vec; use primitive_types::{H256, U256}; use sha3::{Digest, Keccak256}; @@ -313,7 +313,11 @@ pub fn log, H: RuntimeBackend, Tr>( } } - match handler.log(machine.state.as_ref().context.address, topics, data) { + match handler.log(Log { + address: machine.state.as_ref().context.address, + topics, + data, + }) { Ok(()) => Control::Continue, Err(e) => Control::Exit(e.into()), } diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index f563b6c8..29fe0945 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -21,7 +21,7 @@ pub use crate::eval::{Control, Efn, Etable}; pub use crate::memory::Memory; pub use crate::opcode::Opcode; pub use crate::runtime::{ - CallCreateTrap, Context, RuntimeBackend, RuntimeFullBackend, RuntimeState, Transfer, + CallCreateTrap, Context, Log, RuntimeBackend, RuntimeBaseBackend, RuntimeState, Transfer, }; pub use crate::stack::Stack; pub use crate::valids::Valids; diff --git a/interpreter/src/runtime.rs b/interpreter/src/runtime.rs index 8dd6a904..0f77e3d0 100644 --- a/interpreter/src/runtime.rs +++ b/interpreter/src/runtime.rs @@ -1,5 +1,6 @@ use crate::{ExitError, Opcode}; use primitive_types::{H160, H256, U256}; +use sha3::{Digest, Keccak256}; /// Runtime state. #[derive(Clone, Debug)] @@ -46,6 +47,14 @@ pub struct Transfer { pub value: U256, } +/// Log +#[derive(Clone, Debug)] +pub struct Log { + pub address: H160, + pub topics: Vec, + pub data: Vec, +} + pub trait CallCreateTrap: Sized { fn call_create_trap(opcode: Opcode) -> Self; } @@ -56,7 +65,7 @@ impl CallCreateTrap for Opcode { } } -pub trait RuntimeBackend { +pub trait RuntimeBaseBackend { /// Get environmental block hash. fn block_hash(&self, number: U256) -> H256; /// Get environmental block number. @@ -83,18 +92,30 @@ pub trait RuntimeBackend { /// Get balance of address. fn balance(&self, address: H160) -> U256; /// Get code size of address. - fn code_size(&self, address: H160) -> U256; + fn code_size(&self, address: H160) -> U256 { + U256::from(self.code(address).len()) + } /// Get code hash of address. - fn code_hash(&self, address: H160) -> H256; + fn code_hash(&self, address: H160) -> H256 { + H256::from_slice(&Keccak256::digest(&self.code(address)[..])) + } /// Get code of address. fn code(&self, address: H160) -> Vec; /// Get storage value of address at index. fn storage(&self, address: H160, index: H256) -> H256; - /// Get original storage value of address at index. - fn original_storage(&self, address: H160, index: H256) -> H256; /// Check whether an address exists. fn exists(&self, address: H160) -> bool; + + /// Get the current nonce of an account. + fn nonce(&self, address: H160) -> U256; +} + +/// The distinguish between `RuntimeBaseBackend` and `RuntimeBackend` is for the implementation of +/// overlays. +pub trait RuntimeBackend: RuntimeBaseBackend { + /// Get original storage value of address at index. + fn original_storage(&self, address: H160, index: H256) -> H256; /// Check whether an address has already been deleted. fn deleted(&self, address: H160) -> bool; /// Checks if the address or (address, index) pair has been previously accessed. @@ -103,16 +124,10 @@ pub trait RuntimeBackend { fn mark_hot(&mut self, address: H160, index: Option) -> Result<(), ExitError>; /// Set storage value of address at index. fn set_storage(&mut self, address: H160, index: H256, value: H256) -> Result<(), ExitError>; - /// Create a log owned by address with given topics and data. - fn log(&mut self, address: H160, topics: Vec, data: Vec) -> Result<(), ExitError>; + fn log(&mut self, log: Log) -> Result<(), ExitError>; /// Mark an address to be deleted, with funds transferred to target. fn mark_delete(&mut self, address: H160, target: H160) -> Result<(), ExitError>; -} - -pub trait RuntimeFullBackend: RuntimeBackend { - /// Get the current nonce of an account. - fn nonce(&self, address: H160) -> U256; /// Fully delete storages of an account. fn reset_storage(&mut self, address: H160); /// Set code of an account. diff --git a/interpreter/tests/usability.rs b/interpreter/tests/usability.rs index 42fe4981..ffab5fd1 100644 --- a/interpreter/tests/usability.rs +++ b/interpreter/tests/usability.rs @@ -1,6 +1,6 @@ use evm_interpreter::{ - Capture, Context, Control, Etable, ExitError, ExitSucceed, Machine, Opcode, RuntimeBackend, - RuntimeState, + Capture, Context, Control, Etable, ExitError, ExitSucceed, Log, Machine, Opcode, + RuntimeBackend, RuntimeBaseBackend, RuntimeState, Transfer, }; use primitive_types::{H160, H256, U256}; use std::rc::Rc; @@ -57,7 +57,7 @@ fn etable_wrap2() { pub struct UnimplementedHandler; -impl<'a> RuntimeBackend for UnimplementedHandler { +impl<'a> RuntimeBaseBackend for UnimplementedHandler { fn balance(&self, _address: H160) -> U256 { unimplemented!() } @@ -73,9 +73,6 @@ impl<'a> RuntimeBackend for UnimplementedHandler { fn storage(&self, _address: H160, _index: H256) -> H256 { unimplemented!() } - fn original_storage(&self, _address: H160, _index: H256) -> H256 { - unimplemented!() - } fn gas_price(&self) -> U256 { unimplemented!() @@ -114,12 +111,24 @@ impl<'a> RuntimeBackend for UnimplementedHandler { fn exists(&self, _address: H160) -> bool { unimplemented!() } + + fn nonce(&self, _address: H160) -> U256 { + unimplemented!() + } +} + +impl<'a> RuntimeBackend for UnimplementedHandler { + fn original_storage(&self, _address: H160, _index: H256) -> H256 { + unimplemented!() + } + fn deleted(&self, _address: H160) -> bool { unimplemented!() } fn is_cold(&self, _address: H160, _index: Option) -> bool { unimplemented!() } + fn mark_hot(&mut self, _address: H160, _index: Option) -> Result<(), ExitError> { unimplemented!() } @@ -127,12 +136,32 @@ impl<'a> RuntimeBackend for UnimplementedHandler { fn set_storage(&mut self, _address: H160, _index: H256, _value: H256) -> Result<(), ExitError> { unimplemented!() } - fn log(&mut self, _address: H160, _topics: Vec, _data: Vec) -> Result<(), ExitError> { + fn log(&mut self, _log: Log) -> Result<(), ExitError> { unimplemented!() } fn mark_delete(&mut self, _address: H160, _target: H160) -> Result<(), ExitError> { unimplemented!() } + + fn reset_storage(&mut self, _address: H160) { + unimplemented!() + } + + fn set_code(&mut self, _address: H160, _code: Vec) { + unimplemented!() + } + + fn reset_balance(&mut self, _address: H160) { + unimplemented!() + } + + fn transfer(&mut self, _transfer: Transfer) -> Result<(), ExitError> { + unimplemented!() + } + + fn inc_nonce(&mut self, _address: H160) -> Result<(), ExitError> { + unimplemented!() + } } static RUNTIME_ETABLE: Etable = Etable::runtime(); diff --git a/jsontests/Cargo.toml b/jsontests/Cargo.toml new file mode 100644 index 00000000..22818a16 --- /dev/null +++ b/jsontests/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "jsontests" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +evm = { path = ".." } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +primitive-types = { version = "0.12", features = ["rlp", "serde"] } +clap = { version = "4", features = ["derive"] } +thiserror = "1" +hex = { version = "0.4", features = ["serde"] } +ethereum = "0.15.0" +rlp = "0.5" +sha3 = "0.10" diff --git a/jsontests/src/error.rs b/jsontests/src/error.rs new file mode 100644 index 00000000..80b93aac --- /dev/null +++ b/jsontests/src/error.rs @@ -0,0 +1,21 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TestError { + #[error("state root is different")] + StateMismatch, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("io error")] + IO(#[from] std::io::Error), + #[error("json error")] + JSON(#[from] serde_json::Error), + #[error("evm error")] + EVM(#[from] evm::ExitError), + #[error("unsupported fork")] + UnsupportedFork, + #[error("test error")] + Test(#[from] TestError), +} diff --git a/jsontests/src/hash.rs b/jsontests/src/hash.rs new file mode 100644 index 00000000..43ca35b2 --- /dev/null +++ b/jsontests/src/hash.rs @@ -0,0 +1,94 @@ +use evm::backend::in_memory::InMemoryBackend; +use evm::utils::h256_to_u256; +use primitive_types::{H256, U256}; +use sha3::{Digest, Keccak256}; + +/// Basic account type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TrieAccount { + /// Nonce of the account. + pub nonce: U256, + /// Balance of the account. + pub balance: U256, + /// Storage root of the account. + pub storage_root: H256, + /// Code hash of the account. + pub code_hash: H256, + /// Code version of the account. + pub code_version: U256, +} + +impl rlp::Encodable for TrieAccount { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + let use_short_version = self.code_version == U256::zero(); + + match use_short_version { + true => { + stream.begin_list(4); + } + false => { + stream.begin_list(5); + } + } + + stream.append(&self.nonce); + stream.append(&self.balance); + stream.append(&self.storage_root); + stream.append(&self.code_hash); + + if !use_short_version { + stream.append(&self.code_version); + } + } +} + +impl rlp::Decodable for TrieAccount { + fn decode(rlp: &rlp::Rlp) -> Result { + let use_short_version = match rlp.item_count()? { + 4 => true, + 5 => false, + _ => return Err(rlp::DecoderError::RlpIncorrectListLen), + }; + + Ok(TrieAccount { + nonce: rlp.val_at(0)?, + balance: rlp.val_at(1)?, + storage_root: rlp.val_at(2)?, + code_hash: rlp.val_at(3)?, + code_version: if use_short_version { + U256::zero() + } else { + rlp.val_at(4)? + }, + }) + } +} + +pub fn state_root(backend: &InMemoryBackend) -> H256 { + let tree = backend + .current_layer() + .state + .iter() + .map(|(address, account)| { + let storage_root = ethereum::util::sec_trie_root( + account + .storage + .iter() + .map(|(k, v)| (k, rlp::encode(&h256_to_u256(*v)))), + ); + + let code_hash = H256::from_slice(&Keccak256::digest(&account.code)); + let account = TrieAccount { + nonce: account.nonce, + balance: account.balance, + storage_root, + code_hash, + code_version: U256::zero(), + }; + + (address, rlp::encode(&account)) + }) + .collect::>(); + + ethereum::util::sec_trie_root(tree) +} diff --git a/jsontests/src/main.rs b/jsontests/src/main.rs new file mode 100644 index 00000000..2b0c573b --- /dev/null +++ b/jsontests/src/main.rs @@ -0,0 +1,38 @@ +mod error; +mod hash; +mod run; +mod types; + +use crate::error::Error; +use crate::types::*; +use clap::Parser; +use std::collections::BTreeMap; +use std::fs::File; +use std::io::BufReader; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + filename: String, +} + +fn main() -> Result<(), Error> { + let cli = Cli::parse(); + + let test_multi: BTreeMap = + serde_json::from_reader(BufReader::new(File::open(cli.filename)?))?; + + for (test_name, test_multi) in test_multi { + let tests = test_multi.tests(); + + for test in tests { + match crate::run::run_test(&test_name, test) { + Ok(()) => println!("succeed"), + Err(Error::UnsupportedFork) => println!("skipped"), + Err(err) => Err(err)?, + } + } + } + + Ok(()) +} diff --git a/jsontests/src/run.rs b/jsontests/src/run.rs new file mode 100644 index 00000000..95867b8d --- /dev/null +++ b/jsontests/src/run.rs @@ -0,0 +1,94 @@ +use crate::error::{Error, TestError}; +use crate::types::*; +use evm::backend::in_memory::{ + InMemoryAccount, InMemoryBackend, InMemoryEnvironment, InMemoryLayer, +}; +use evm::standard::{Config, Etable, Invoker}; +use evm::utils::u256_to_h256; +use primitive_types::U256; +use std::collections::{BTreeMap, BTreeSet}; + +pub fn run_test(test_name: &str, test: Test) -> Result<(), Error> { + println!( + "test name: {}, fork: {:?}, index: {}", + test_name, test.fork, test.index + ); + + let config = match test.fork { + Fork::Berlin => Config::berlin(), + _ => return Err(Error::UnsupportedFork), + }; + + let env = InMemoryEnvironment { + block_hashes: BTreeMap::new(), // TODO: fill in this field. + block_number: test.env.current_number, + block_coinbase: test.env.current_coinbase, + block_timestamp: test.env.current_timestamp, + block_difficulty: test.env.current_difficulty, + block_randomness: Some(test.env.current_random), + block_gas_limit: test.env.current_gas_limit, + block_base_fee_per_gas: U256::zero(), // TODO: fill in this field. + chain_id: U256::zero(), // TODO: fill in this field. + gas_price: test.transaction.gas_price, + origin: test.transaction.sender, + }; + + let state = test + .pre + .clone() + .into_iter() + .map(|(address, account)| { + let storage = account + .storage + .into_iter() + .map(|(key, value)| (u256_to_h256(key), u256_to_h256(value))) + .collect::>(); + + ( + address, + InMemoryAccount { + balance: account.balance, + code: account.code.0, + nonce: account.nonce, + original_storage: storage.clone(), + storage, + }, + ) + }) + .collect::>(); + + let mut backend = InMemoryBackend { + environment: env, + layers: vec![InMemoryLayer { + state, + logs: Vec::new(), + suicides: Vec::new(), + hots: BTreeSet::new(), + }], + }; + + let etable = Etable::runtime(); + let invoker = Invoker::new(&config); + let result = invoker.transact_call( + test.transaction.sender, + test.transaction.to, + test.transaction.value, + test.transaction.data, + test.transaction.gas_limit, + test.transaction.gas_price, + Vec::new(), + &mut backend, + &etable, + ); + + let state_root = crate::hash::state_root(&backend); + + println!("result: {:?}", result); + println!("state root: {:?}", state_root); + + if state_root != test.post.hash { + return Err(TestError::StateMismatch.into()); + } + + Ok(()) +} diff --git a/jsontests/src/types.rs b/jsontests/src/types.rs new file mode 100644 index 00000000..ad70c79e --- /dev/null +++ b/jsontests/src/types.rs @@ -0,0 +1,189 @@ +use hex::FromHex; +use primitive_types::{H160, H256, U256}; +use serde::{ + de::{Error, Visitor}, + Deserialize, Deserializer, +}; +use std::collections::BTreeMap; +use std::fmt; + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +pub struct TestMulti { + #[serde(rename = "_info")] + pub info: TestInfo, + pub env: TestEnv, + pub post: BTreeMap>, + pub pre: BTreeMap, + pub transaction: TestMultiTransaction, +} + +impl TestMulti { + pub fn tests(&self) -> Vec { + let mut tests = Vec::new(); + + for (fork, post_states) in &self.post { + for (index, post_state) in post_states.iter().enumerate() { + tests.push(Test { + info: self.info.clone(), + env: self.env.clone(), + fork: fork.clone(), + index, + post: post_state.clone(), + pre: self.pre.clone(), + transaction: TestTransaction { + data: self.transaction.data[post_state.indexes.data].0.clone(), + gas_limit: self.transaction.gas_limit[post_state.indexes.gas], + gas_price: self.transaction.gas_price, + nonce: self.transaction.nonce, + secret_key: self.transaction.secret_key, + sender: self.transaction.sender, + to: self.transaction.to, + value: self.transaction.value[post_state.indexes.value], + }, + }); + } + } + + tests + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Test { + pub info: TestInfo, + pub env: TestEnv, + pub fork: Fork, + pub index: usize, + pub post: TestPostState, + pub pre: BTreeMap, + pub transaction: TestTransaction, +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TestInfo { + pub comment: String, + #[serde(rename = "filling-rpc-server")] + pub filling_rpc_server: String, + #[serde(rename = "filling-tool-version")] + pub filling_tool_version: String, + pub generated_test_hash: String, + pub lllcversion: String, + pub solidity: String, + pub source: String, + pub source_hash: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TestEnv { + pub current_base_fee: U256, + pub current_beacon_root: H256, + pub current_coinbase: H160, + pub current_difficulty: U256, + pub current_gas_limit: U256, + pub current_number: U256, + pub current_random: H256, + pub current_timestamp: U256, + pub current_withdrawals_root: H256, + pub previous_hash: H256, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Deserialize)] +pub enum Fork { + Berlin, + Cancun, + London, + Merge, + Shanghai, +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +pub struct TestPostState { + pub hash: H256, + pub indexes: TestPostStateIndexes, + pub logs: H256, + pub txbytes: HexBytes, +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +pub struct TestPostStateIndexes { + pub data: usize, + pub gas: usize, + pub value: usize, +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +pub struct TestPreState { + pub balance: U256, + pub code: HexBytes, + pub nonce: U256, + pub storage: BTreeMap, +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TestMultiTransaction { + pub data: Vec, + pub gas_limit: Vec, + pub gas_price: U256, + pub nonce: U256, + pub secret_key: H256, + pub sender: H160, + pub to: H160, + pub value: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TestTransaction { + pub data: Vec, + pub gas_limit: U256, + pub gas_price: U256, + pub nonce: U256, + pub secret_key: H256, + pub sender: H160, + pub to: H160, + pub value: U256, +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +pub struct HexBytes(#[serde(deserialize_with = "deserialize_hex_bytes")] pub Vec); + +fn deserialize_hex_bytes<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct HexStrVisitor; + + impl<'de> Visitor<'de> for HexStrVisitor { + type Value = Vec; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "a hex encoded string") + } + + fn visit_str(self, data: &str) -> Result + where + E: Error, + { + if &data[0..2] != "0x" { + return Err(Error::custom("should start with 0x")); + } + + FromHex::from_hex(&data[2..]).map_err(Error::custom) + } + + fn visit_borrowed_str(self, data: &'de str) -> Result + where + E: Error, + { + if &data[0..2] != "0x" { + return Err(Error::custom("should start with 0x")); + } + + FromHex::from_hex(&data[2..]).map_err(Error::custom) + } + } + + deserializer.deserialize_str(HexStrVisitor) +} diff --git a/src/backend/in_memory.rs b/src/backend/in_memory.rs new file mode 100644 index 00000000..98242255 --- /dev/null +++ b/src/backend/in_memory.rs @@ -0,0 +1,273 @@ +use crate::{ + ExitError, ExitException, Log, RuntimeBackend, RuntimeBaseBackend, TransactionalBackend, + TransactionalMergeStrategy, Transfer, +}; +use alloc::collections::{BTreeMap, BTreeSet}; +use primitive_types::{H160, H256, U256}; + +#[derive(Clone, Debug)] +pub struct InMemoryEnvironment { + pub block_hashes: BTreeMap, + pub block_number: U256, + pub block_coinbase: H160, + pub block_timestamp: U256, + pub block_difficulty: U256, + pub block_randomness: Option, + pub block_gas_limit: U256, + pub block_base_fee_per_gas: U256, + pub chain_id: U256, + pub gas_price: U256, + pub origin: H160, +} + +#[derive(Clone, Debug, Default)] +pub struct InMemoryAccount { + pub balance: U256, + pub code: Vec, + pub nonce: U256, + pub storage: BTreeMap, + pub original_storage: BTreeMap, +} + +#[derive(Clone, Debug)] +pub struct InMemorySuicideInfo { + pub address: H160, + pub target: H160, +} + +#[derive(Clone, Debug)] +pub struct InMemoryLayer { + pub state: BTreeMap, + pub logs: Vec, + pub suicides: Vec, + pub hots: BTreeSet<(H160, Option)>, +} + +#[derive(Clone, Debug)] +pub struct InMemoryBackend { + pub environment: InMemoryEnvironment, + pub layers: Vec, +} + +impl InMemoryBackend { + pub fn current_layer(&self) -> &InMemoryLayer { + self.layers.last().expect("current layer exists") + } + + pub fn current_layer_mut(&mut self) -> &mut InMemoryLayer { + self.layers.last_mut().expect("current layer exists") + } +} + +impl RuntimeBaseBackend for InMemoryBackend { + fn block_hash(&self, number: U256) -> H256 { + self.environment + .block_hashes + .get(&number) + .cloned() + .unwrap_or(H256::default()) + } + + fn block_number(&self) -> U256 { + self.environment.block_number + } + + fn block_coinbase(&self) -> H160 { + self.environment.block_coinbase + } + + fn block_timestamp(&self) -> U256 { + self.environment.block_timestamp + } + + fn block_difficulty(&self) -> U256 { + self.environment.block_difficulty + } + + fn block_randomness(&self) -> Option { + self.environment.block_randomness + } + + fn block_gas_limit(&self) -> U256 { + self.environment.block_gas_limit + } + + fn block_base_fee_per_gas(&self) -> U256 { + self.environment.block_base_fee_per_gas + } + + fn chain_id(&self) -> U256 { + self.environment.chain_id + } + + fn gas_price(&self) -> U256 { + self.environment.gas_price + } + + fn origin(&self) -> H160 { + self.environment.origin + } + + fn balance(&self, address: H160) -> U256 { + self.current_layer() + .state + .get(&address) + .cloned() + .unwrap_or(Default::default()) + .balance + } + + fn code(&self, address: H160) -> Vec { + self.current_layer() + .state + .get(&address) + .cloned() + .unwrap_or(Default::default()) + .code + } + + fn exists(&self, address: H160) -> bool { + self.current_layer().state.get(&address).is_some() + } + + fn storage(&self, address: H160, index: H256) -> H256 { + self.current_layer() + .state + .get(&address) + .cloned() + .unwrap_or(Default::default()) + .storage + .get(&index) + .cloned() + .unwrap_or(H256::default()) + } + + fn nonce(&self, address: H160) -> U256 { + self.current_layer() + .state + .get(&address) + .cloned() + .unwrap_or(Default::default()) + .nonce + } +} + +impl RuntimeBackend for InMemoryBackend { + fn original_storage(&self, address: H160, index: H256) -> H256 { + self.current_layer() + .state + .get(&address) + .cloned() + .unwrap_or(Default::default()) + .storage + .get(&index) + .cloned() + .unwrap_or(H256::default()) + } + + fn deleted(&self, address: H160) -> bool { + self.current_layer() + .suicides + .iter() + .any(|suicide| suicide.address == address) + } + + fn is_cold(&self, address: H160, index: Option) -> bool { + !self.current_layer().hots.contains(&(address, index)) + } + + fn mark_hot(&mut self, address: H160, index: Option) -> Result<(), ExitError> { + self.current_layer_mut().hots.insert((address, index)); + Ok(()) + } + + fn set_storage(&mut self, address: H160, index: H256, value: H256) -> Result<(), ExitError> { + self.current_layer_mut() + .state + .entry(address) + .or_default() + .storage + .insert(index, value); + Ok(()) + } + + fn log(&mut self, log: Log) -> Result<(), ExitError> { + self.current_layer_mut().logs.push(log); + Ok(()) + } + + fn mark_delete(&mut self, address: H160, target: H160) -> Result<(), ExitError> { + self.current_layer_mut() + .suicides + .push(InMemorySuicideInfo { address, target }); + Ok(()) + } + + fn reset_storage(&mut self, address: H160) { + self.current_layer_mut() + .state + .entry(address) + .or_default() + .storage = Default::default(); + } + + fn set_code(&mut self, address: H160, code: Vec) { + self.current_layer_mut() + .state + .entry(address) + .or_default() + .code = code; + } + + fn reset_balance(&mut self, address: H160) { + self.current_layer_mut() + .state + .entry(address) + .or_default() + .balance = U256::zero(); + } + + fn transfer(&mut self, transfer: Transfer) -> Result<(), ExitError> { + { + let source = self + .current_layer_mut() + .state + .entry(transfer.source) + .or_default(); + if source.balance < transfer.value { + return Err(ExitException::OutOfFund.into()); + } + source.balance -= transfer.value; + } + self.current_layer_mut() + .state + .entry(transfer.target) + .or_default() + .balance += transfer.value; + Ok(()) + } + + fn inc_nonce(&mut self, address: H160) -> Result<(), ExitError> { + let entry = self.current_layer_mut().state.entry(address).or_default(); + entry.nonce = entry.nonce.saturating_add(U256::one()); + Ok(()) + } +} + +impl TransactionalBackend for InMemoryBackend { + fn push_substate(&mut self) { + let layer = self.current_layer().clone(); + self.layers.push(layer); + } + + fn pop_substate(&mut self, strategy: TransactionalMergeStrategy) { + let layer = self.layers.pop().expect("current layer exist"); + + match strategy { + TransactionalMergeStrategy::Commit => { + *self.current_layer_mut() = layer; + } + TransactionalMergeStrategy::Discard => (), + } + } +} diff --git a/src/backend.rs b/src/backend/mod.rs similarity index 90% rename from src/backend.rs rename to src/backend/mod.rs index ebb468ae..359a1637 100644 --- a/src/backend.rs +++ b/src/backend/mod.rs @@ -1,3 +1,5 @@ +pub mod in_memory; + pub enum TransactionalMergeStrategy { Commit, Discard, diff --git a/src/lib.rs b/src/lib.rs index 65eb5b47..fade6a34 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,9 +6,9 @@ extern crate alloc; +pub mod backend; pub mod standard; -mod backend; mod call_stack; mod gasometer; mod invoker; diff --git a/src/standard/invoker.rs b/src/standard/invoker.rs index 1dac81b7..a919bb84 100644 --- a/src/standard/invoker.rs +++ b/src/standard/invoker.rs @@ -2,7 +2,7 @@ use super::{gasometer::TransactionCost, Config, Etable, GasedMachine, Gasometer, use crate::call_create::{CallCreateTrapData, CallTrapData, CreateTrapData}; use crate::{ Capture, Context, ExitError, ExitException, ExitResult, Gasometer as GasometerT, - GasometerMergeStrategy, Invoker as InvokerT, Opcode, RuntimeFullBackend, RuntimeState, + GasometerMergeStrategy, Invoker as InvokerT, Opcode, RuntimeBackend, RuntimeState, TransactionalBackend, TransactionalMergeStrategy, Transfer, }; use alloc::rc::Rc; @@ -35,6 +35,10 @@ pub struct Invoker<'config> { } impl<'config> Invoker<'config> { + pub fn new(config: &'config Config) -> Self { + Self { config } + } + pub fn transact_call( &self, caller: H160, @@ -42,46 +46,54 @@ impl<'config> Invoker<'config> { value: U256, data: Vec, gas_limit: U256, + gas_price: U256, access_list: Vec<(H160, Vec)>, handler: &mut H, etable: &Etable, ) -> ExitResult where - H: RuntimeFullBackend + TransactionalBackend, + H: RuntimeBackend + TransactionalBackend, { - let gas_limit = if gas_limit > U256::from(u64::MAX) { - return Err(ExitException::OutOfGas.into()); - } else { - gas_limit.as_u64() - }; + handler.push_substate(); - let context = Context { - caller, - address, - apparent_value: value, - }; - let code = handler.code(address); + let work = || -> ExitResult { + let gas_fee = gas_limit.saturating_mul(gas_price); + handler.transfer(Transfer { + source: caller, + target: handler.block_coinbase(), + value: gas_fee, + })?; + + let gas_limit = if gas_limit > U256::from(u64::MAX) { + return Err(ExitException::OutOfGas.into()); + } else { + gas_limit.as_u64() + }; - let transaction_cost = TransactionCost::call(&data, &access_list).cost(self.config); + let context = Context { + caller, + address, + apparent_value: value, + }; + let code = handler.code(address); - let machine = Machine::new( - Rc::new(code), - Rc::new(data), - self.config.stack_limit, - self.config.memory_limit, - RuntimeState { - context, - retbuf: Vec::new(), - gas: U256::zero(), - }, - ); - let mut gasometer = Gasometer::new(gas_limit, &machine, self.config); + let transaction_cost = TransactionCost::call(&data, &access_list).cost(self.config); - gasometer.record_cost(transaction_cost)?; + let machine = Machine::new( + Rc::new(code), + Rc::new(data), + self.config.stack_limit, + self.config.memory_limit, + RuntimeState { + context, + retbuf: Vec::new(), + gas: U256::zero(), + }, + ); + let mut gasometer = Gasometer::new(gas_limit, &machine, self.config); - handler.push_substate(); + gasometer.record_cost(transaction_cost)?; - let work = || -> ExitResult { if self.config.increase_state_access_gas { if self.config.warm_coinbase_address { let coinbase = handler.block_coinbase(); @@ -106,9 +118,17 @@ impl<'config> Invoker<'config> { is_static: false, }; - let (_machine, result) = + let (machine, result) = crate::execute(machine, handler, 0, DEFAULT_HEAP_DEPTH, self, etable); + let refunded_gas = U256::from(machine.gasometer.gas()); + let refunded_fee = refunded_gas * gas_price; + handler.transfer(Transfer { + source: handler.block_coinbase(), + target: caller, + value: refunded_fee, + })?; + result }; @@ -127,7 +147,7 @@ impl<'config> Invoker<'config> { impl<'config, H> InvokerT, H, Opcode> for Invoker<'config> where - H: RuntimeFullBackend + TransactionalBackend, + H: RuntimeBackend + TransactionalBackend, { type Interrupt = Infallible; type CallCreateTrapPrepareData = CallCreateTrapPrepareData; @@ -300,7 +320,7 @@ fn enter_create_trap_stack<'config, H>( config: &'config Config, ) -> Result<(CallCreateTrapEnterData, GasedMachine<'config>), ExitError> where - H: RuntimeFullBackend + TransactionalBackend, + H: RuntimeBackend + TransactionalBackend, { handler.push_substate(); @@ -391,7 +411,7 @@ fn exit_create_trap_stack_no_exit_substate<'config, H>( config: &'config Config, ) -> Result where - H: RuntimeFullBackend + TransactionalBackend, + H: RuntimeBackend + TransactionalBackend, { fn check_first_byte(config: &Config, code: &[u8]) -> Result<(), ExitError> { if config.disallow_executable_format && Some(&Opcode::EOFMAGIC.as_u8()) == code.first() { @@ -424,7 +444,7 @@ fn enter_call_trap_stack<'config, H>( config: &'config Config, ) -> Result<(CallCreateTrapEnterData, GasedMachine<'config>), ExitError> where - H: RuntimeFullBackend + TransactionalBackend, + H: RuntimeBackend + TransactionalBackend, { handler.push_substate();