From f9a08e285c811796d0c52d4fd62c3caccb068374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Mon, 28 Aug 2017 14:25:16 +0200 Subject: [PATCH] Running state test using parity-evm (#6355) * Initial version of state tests. * Refactor state to support tracing. * Unify TransactResult. * Add test. --- Cargo.lock | 1 + ethcore/src/client/client.rs | 60 +++++--- ethcore/src/client/evm_test_client.rs | 189 +++++++++++++++++++++----- ethcore/src/client/mod.rs | 2 +- ethcore/src/executive.rs | 113 +++++++++++---- ethcore/src/json_tests/state.rs | 72 +++++----- ethcore/src/state/mod.rs | 57 ++++++-- ethcore/src/tests/client.rs | 4 +- ethcore/src/trace/executive_tracer.rs | 2 +- ethcore/src/trace/mod.rs | 2 +- ethcore/src/trace/noop_tracer.rs | 2 +- evmbin/Cargo.toml | 1 + evmbin/src/display/json.rs | 14 +- evmbin/src/display/simple.rs | 6 +- evmbin/src/info.rs | 74 ++++++++-- evmbin/src/main.rs | 93 +++++++++++-- json/src/state/test.rs | 5 +- 17 files changed, 538 insertions(+), 159 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 913ac0e44..596c32060 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -935,6 +935,7 @@ dependencies = [ "docopt 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "ethcore 1.8.0", "ethcore-util 1.8.0", + "ethjson 0.1.0", "evm 0.1.0", "panic_hook 0.1.0", "rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/ethcore/src/client/client.rs b/ethcore/src/client/client.rs index ff09a7b2b..ac8a0aea9 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -748,7 +748,7 @@ impl Client { self.factories.clone(), ).expect("state known to be available for just-imported block; qed"); - let options = TransactOptions { tracing: false, vm_tracing: false, check_nonce: false }; + let options = TransactOptions::with_no_tracing().dont_check_nonce(); let res = Executive::new(&mut state, &env_info, &*self.engine) .transact(&transaction, options); @@ -1113,18 +1113,39 @@ impl Client { }.fake_sign(from) } - fn do_call(&self, env_info: &EnvInfo, state: &mut State, t: &SignedTransaction, analytics: CallAnalytics) -> Result { - let original_state = if analytics.state_diffing { Some(state.clone()) } else { None }; + fn do_virtual_call(&self, env_info: &EnvInfo, state: &mut State, t: &SignedTransaction, analytics: CallAnalytics) -> Result { + fn call( + state: &mut State, + env_info: &EnvInfo, + engine: &E, + state_diff: bool, + transaction: &SignedTransaction, + options: TransactOptions, + ) -> Result where + E: Engine + ?Sized, + T: trace::Tracer, + V: trace::VMTracer, + { + let options = options.dont_check_nonce(); + let original_state = if state_diff { Some(state.clone()) } else { None }; - let options = TransactOptions { tracing: analytics.transaction_tracing, vm_tracing: analytics.vm_tracing, check_nonce: false }; - let mut ret = Executive::new(state, env_info, &*self.engine).transact_virtual(t, options)?; + let mut ret = Executive::new(state, env_info, engine).transact_virtual(transaction, options)?; - // TODO gav move this into Executive. - if let Some(original) = original_state { - ret.state_diff = Some(state.diff_from(original).map_err(ExecutionError::from)?); + if let Some(original) = original_state { + ret.state_diff = Some(state.diff_from(original).map_err(ExecutionError::from)?); + } + Ok(ret) } - Ok(ret) + let state_diff = analytics.state_diffing; + let engine = &*self.engine; + + match (analytics.transaction_tracing, analytics.vm_tracing) { + (true, true) => call(state, env_info, engine, state_diff, t, TransactOptions::with_tracing_and_vm_tracing()), + (true, false) => call(state, env_info, engine, state_diff, t, TransactOptions::with_tracing()), + (false, true) => call(state, env_info, engine, state_diff, t, TransactOptions::with_vm_tracing()), + (false, false) => call(state, env_info, engine, state_diff, t, TransactOptions::with_no_tracing()), + } } } @@ -1157,7 +1178,7 @@ impl BlockChainClient for Client { // that's just a copy of the state. let mut state = self.state_at(block).ok_or(CallError::StatePruned)?; - self.do_call(&env_info, &mut state, transaction, analytics) + self.do_virtual_call(&env_info, &mut state, transaction, analytics) } fn call_many(&self, transactions: &[(SignedTransaction, CallAnalytics)], block: BlockId) -> Result, CallError> { @@ -1169,7 +1190,7 @@ impl BlockChainClient for Client { let mut results = Vec::with_capacity(transactions.len()); for &(ref t, analytics) in transactions { - let ret = self.do_call(&env_info, &mut state, t, analytics)?; + let ret = self.do_virtual_call(&env_info, &mut state, t, analytics)?; env_info.gas_used = ret.cumulative_gas_used; results.push(ret); } @@ -1189,7 +1210,7 @@ impl BlockChainClient for Client { // that's just a copy of the state. let original_state = self.state_at(block).ok_or(CallError::StatePruned)?; let sender = t.sender(); - let options = TransactOptions { tracing: true, vm_tracing: false, check_nonce: false }; + let options = || TransactOptions::with_tracing(); let cond = |gas| { let mut tx = t.as_unsigned().clone(); @@ -1198,7 +1219,7 @@ impl BlockChainClient for Client { let mut state = original_state.clone(); Ok(Executive::new(&mut state, &env_info, &*self.engine) - .transact_virtual(&tx, options.clone()) + .transact_virtual(&tx, options()) .map(|r| r.exception.is_none()) .unwrap_or(false)) }; @@ -1254,22 +1275,17 @@ impl BlockChainClient for Client { return Err(CallError::TransactionNotFound); } - let options = TransactOptions { tracing: analytics.transaction_tracing, vm_tracing: analytics.vm_tracing, check_nonce: false }; const PROOF: &'static str = "Transactions fetched from blockchain; blockchain transactions are valid; qed"; let rest = txs.split_off(address.index); for t in txs { let t = SignedTransaction::new(t).expect(PROOF); - let x = Executive::new(&mut state, &env_info, &*self.engine).transact(&t, Default::default())?; + let x = Executive::new(&mut state, &env_info, &*self.engine).transact(&t, TransactOptions::with_no_tracing())?; env_info.gas_used = env_info.gas_used + x.gas_used; } let first = rest.into_iter().next().expect("We split off < `address.index`; Length is checked earlier; qed"); let t = SignedTransaction::new(first).expect(PROOF); - let original_state = if analytics.state_diffing { Some(state.clone()) } else { None }; - let mut ret = Executive::new(&mut state, &env_info, &*self.engine).transact(&t, options)?; - if let Some(original) = original_state { - ret.state_diff = Some(state.diff_from(original).map_err(ExecutionError::from)?) - } - Ok(ret) + + self.do_virtual_call(&env_info, &mut state, &t, analytics) } fn mode(&self) -> IpcMode { @@ -1951,7 +1967,7 @@ impl ProvingBlockChainClient for Client { let backend = state::backend::Proving::new(jdb.as_hashdb_mut()); let mut state = state.replace_backend(backend); - let options = TransactOptions { tracing: false, vm_tracing: false, check_nonce: false }; + let options = TransactOptions::with_no_tracing().dont_check_nonce(); let res = Executive::new(&mut state, &env_info, &*self.engine).transact(&transaction, options); match res { diff --git a/ethcore/src/client/evm_test_client.rs b/ethcore/src/client/evm_test_client.rs index cd8501c31..a455a3724 100644 --- a/ethcore/src/client/evm_test_client.rs +++ b/ethcore/src/client/evm_test_client.rs @@ -18,9 +18,9 @@ use std::fmt; use std::sync::Arc; -use util::{self, U256, journaldb, trie}; +use util::{self, U256, H256, journaldb, trie}; use util::kvdb::{self, KeyValueDB}; -use {state, state_db, client, executive, trace, db, spec}; +use {state, state_db, client, executive, trace, transaction, db, spec, pod_state}; use factory::Factories; use evm::{self, VMType}; use vm::{self, ActionParams}; @@ -33,9 +33,17 @@ pub enum EvmTestError { /// EVM error. Evm(vm::Error), /// Initialization error. - Initialization(::error::Error), + ClientError(::error::Error), /// Low-level database error. Database(String), + /// Post-condition failure, + PostCondition(String), +} + +impl> From for EvmTestError { + fn from(err: E) -> Self { + EvmTestError::ClientError(err.into()) + } } impl fmt::Display for EvmTestError { @@ -45,52 +53,114 @@ impl fmt::Display for EvmTestError { match *self { Trie(ref err) => write!(fmt, "Trie: {}", err), Evm(ref err) => write!(fmt, "EVM: {}", err), - Initialization(ref err) => write!(fmt, "Initialization: {}", err), + ClientError(ref err) => write!(fmt, "{}", err), Database(ref err) => write!(fmt, "DB: {}", err), + PostCondition(ref err) => write!(fmt, "{}", err), } } } -/// Simplified, single-block EVM test client. -pub struct EvmTestClient { - state_db: state_db::StateDB, - factories: Factories, - spec: spec::Spec, +use ethereum; +use ethjson::state::test::ForkSpec; + +lazy_static! { + pub static ref FRONTIER: spec::Spec = ethereum::new_frontier_test(); + pub static ref HOMESTEAD: spec::Spec = ethereum::new_homestead_test(); + pub static ref EIP150: spec::Spec = ethereum::new_eip150_test(); + pub static ref EIP161: spec::Spec = ethereum::new_eip161_test(); + pub static ref _METROPOLIS: spec::Spec = ethereum::new_metropolis_test(); } -impl EvmTestClient { - /// Creates new EVM test client with in-memory DB initialized with genesis of given Spec. - pub fn new(spec: spec::Spec) -> Result { - let factories = Factories { - vm: evm::Factory::new(VMType::Interpreter, 5 * 1024), - trie: trie::TrieFactory::new(trie::TrieSpec::Secure), - accountdb: Default::default(), - }; - let db = Arc::new(kvdb::in_memory(db::NUM_COLUMNS.expect("We use column-based DB; qed"))); - let journal_db = journaldb::new(db.clone(), journaldb::Algorithm::EarlyMerge, db::COL_STATE); - let mut state_db = state_db::StateDB::new(journal_db, 5 * 1024 * 1024); - state_db = spec.ensure_db_good(state_db, &factories).map_err(EvmTestError::Initialization)?; - // Write DB - { - let mut batch = kvdb::DBTransaction::new(); - state_db.journal_under(&mut batch, 0, &spec.genesis_header().hash()).map_err(|e| EvmTestError::Initialization(e.into()))?; - db.write(batch).map_err(EvmTestError::Database)?; +/// Simplified, single-block EVM test client. +pub struct EvmTestClient<'a> { + state: state::State, + spec: &'a spec::Spec, +} + +impl<'a> EvmTestClient<'a> { + /// Converts a json spec definition into spec. + pub fn spec_from_json(spec: &ForkSpec) -> Option<&'static spec::Spec> { + match *spec { + ForkSpec::Frontier => Some(&*FRONTIER), + ForkSpec::Homestead => Some(&*HOMESTEAD), + ForkSpec::EIP150 => Some(&*EIP150), + ForkSpec::EIP158 => Some(&*EIP161), + ForkSpec::Metropolis | ForkSpec::Byzantium | ForkSpec::Constantinople => None, } + } + + /// Creates new EVM test client with in-memory DB initialized with genesis of given Spec. + pub fn new(spec: &'a spec::Spec) -> Result { + let factories = Self::factories(); + let state = Self::state_from_spec(spec, &factories)?; Ok(EvmTestClient { - state_db, - factories, + state, spec, }) } - /// Call given contract. + /// Creates new EVM test client with in-memory DB initialized with given PodState. + pub fn from_pod_state(spec: &'a spec::Spec, pod_state: pod_state::PodState) -> Result { + let factories = Self::factories(); + let state = Self::state_from_pod(spec, &factories, pod_state)?; + + Ok(EvmTestClient { + state, + spec, + }) + } + + fn factories() -> Factories { + Factories { + vm: evm::Factory::new(VMType::Interpreter, 5 * 1024), + trie: trie::TrieFactory::new(trie::TrieSpec::Secure), + accountdb: Default::default(), + } + } + + fn state_from_spec(spec: &'a spec::Spec, factories: &Factories) -> Result, EvmTestError> { + let db = Arc::new(kvdb::in_memory(db::NUM_COLUMNS.expect("We use column-based DB; qed"))); + let journal_db = journaldb::new(db.clone(), journaldb::Algorithm::EarlyMerge, db::COL_STATE); + let mut state_db = state_db::StateDB::new(journal_db, 5 * 1024 * 1024); + state_db = spec.ensure_db_good(state_db, factories)?; + + let genesis = spec.genesis_header(); + // Write DB + { + let mut batch = kvdb::DBTransaction::new(); + state_db.journal_under(&mut batch, 0, &genesis.hash())?; + db.write(batch).map_err(EvmTestError::Database)?; + } + + state::State::from_existing( + state_db, + *genesis.state_root(), + spec.engine.account_start_nonce(0), + factories.clone() + ).map_err(EvmTestError::Trie) + } + + fn state_from_pod(spec: &'a spec::Spec, factories: &Factories, pod_state: pod_state::PodState) -> Result, EvmTestError> { + let db = Arc::new(kvdb::in_memory(db::NUM_COLUMNS.expect("We use column-based DB; qed"))); + let journal_db = journaldb::new(db.clone(), journaldb::Algorithm::EarlyMerge, db::COL_STATE); + let state_db = state_db::StateDB::new(journal_db, 5 * 1024 * 1024); + let mut state = state::State::new( + state_db, + spec.engine.account_start_nonce(0), + factories.clone(), + ); + state.populate_from(pod_state); + state.commit()?; + Ok(state) + } + + /// Execute the VM given ActionParams and tracer. + /// Returns amount of gas left and the output. pub fn call(&mut self, params: ActionParams, vm_tracer: &mut T) -> Result<(U256, Vec), EvmTestError> { let genesis = self.spec.genesis_header(); - let mut state = state::State::from_existing(self.state_db.boxed_clone(), *genesis.state_root(), self.spec.engine.account_start_nonce(0), self.factories.clone()) - .map_err(EvmTestError::Trie)?; let info = client::EnvInfo { number: genesis.number(), author: *genesis.author(), @@ -103,7 +173,7 @@ impl EvmTestClient { let mut substate = state::Substate::new(); let mut tracer = trace::NoopTracer; let mut output = vec![]; - let mut executive = executive::Executive::new(&mut state, &info, &*self.spec.engine); + let mut executive = executive::Executive::new(&mut self.state, &info, &*self.spec.engine); let (gas_left, _) = executive.call( params, &mut substate, @@ -114,4 +184,59 @@ impl EvmTestClient { Ok((gas_left, output)) } + + /// Executes a SignedTransaction within context of the provided state and `EnvInfo`. + /// Returns the state root, gas left and the output. + pub fn transact( + &mut self, + env_info: &client::EnvInfo, + transaction: transaction::SignedTransaction, + vm_tracer: T, + ) -> TransactResult { + let initial_gas = transaction.gas; + // Verify transaction + let is_ok = transaction.verify_basic(true, None, env_info.number >= self.spec.engine.params().eip86_transition); + if let Err(error) = is_ok { + return TransactResult::Err { + state_root: *self.state.root(), + error, + }; + } + + // Apply transaction + let tracer = trace::NoopTracer; + let result = self.state.apply_with_tracing(&env_info, &*self.spec.engine, &transaction, tracer, vm_tracer); + + match result { + Ok(result) => TransactResult::Ok { + state_root: *self.state.root(), + gas_left: initial_gas - result.receipt.gas_used, + output: result.output + }, + Err(error) => TransactResult::Err { + state_root: *self.state.root(), + error, + }, + } + } +} + +/// A result of applying transaction to the state. +pub enum TransactResult { + /// Successful execution + Ok { + /// State root + state_root: H256, + /// Amount of gas left + gas_left: U256, + /// Output + output: Vec, + }, + /// Transaction failed to run + Err { + /// State root + state_root: H256, + /// Execution error + error: ::error::Error, + }, } diff --git a/ethcore/src/client/mod.rs b/ethcore/src/client/mod.rs index f7c7417ad..9377c2f44 100644 --- a/ethcore/src/client/mod.rs +++ b/ethcore/src/client/mod.rs @@ -27,7 +27,7 @@ mod client; pub use self::client::*; pub use self::config::{Mode, ClientConfig, DatabaseCompactionProfile, BlockChainConfig, VMType}; pub use self::error::Error; -pub use self::evm_test_client::{EvmTestClient, EvmTestError}; +pub use self::evm_test_client::{EvmTestClient, EvmTestError, TransactResult}; pub use self::test_client::{TestBlockChainClient, EachBlockWith}; pub use self::chain_notify::ChainNotify; pub use self::traits::{BlockChainClient, MiningBlockChainClient, EngineClient}; diff --git a/ethcore/src/executive.rs b/ethcore/src/executive.rs index 8f2fb06f2..66a770243 100644 --- a/ethcore/src/executive.rs +++ b/ethcore/src/executive.rs @@ -26,7 +26,7 @@ use evm::{CallType, Factory, Finalize, FinalizationResult}; use vm::{self, Ext, CreateContractAddress, ReturnData, CleanDustMode, ActionParams, ActionValue}; use wasm; use externalities::*; -use trace::{FlatTrace, Tracer, NoopTracer, ExecutiveTracer, VMTrace, VMTracer, ExecutiveVMTracer, NoopVMTracer}; +use trace::{self, FlatTrace, VMTrace, Tracer, VMTracer}; use transaction::{Action, SignedTransaction}; use crossbeam; pub use executed::{Executed, ExecutionResult}; @@ -66,16 +66,77 @@ pub fn contract_address(address_scheme: CreateContractAddress, sender: &Address, } /// Transaction execution options. -#[derive(Default, Copy, Clone, PartialEq)] -pub struct TransactOptions { +#[derive(Copy, Clone, PartialEq)] +pub struct TransactOptions { /// Enable call tracing. - pub tracing: bool, + pub tracer: T, /// Enable VM tracing. - pub vm_tracing: bool, + pub vm_tracer: V, /// Check transaction nonce before execution. pub check_nonce: bool, } +impl TransactOptions { + /// Create new `TransactOptions` with given tracer and VM tracer. + pub fn new(tracer: T, vm_tracer: V) -> Self { + TransactOptions { + tracer, + vm_tracer, + check_nonce: true, + } + } + + /// Disables the nonce check + pub fn dont_check_nonce(mut self) -> Self { + self.check_nonce = false; + self + } +} + +impl TransactOptions { + /// Creates new `TransactOptions` with default tracing and VM tracing. + pub fn with_tracing_and_vm_tracing() -> Self { + TransactOptions { + tracer: trace::ExecutiveTracer::default(), + vm_tracer: trace::ExecutiveVMTracer::toplevel(), + check_nonce: true, + } + } +} + +impl TransactOptions { + /// Creates new `TransactOptions` with default tracing and no VM tracing. + pub fn with_tracing() -> Self { + TransactOptions { + tracer: trace::ExecutiveTracer::default(), + vm_tracer: trace::NoopVMTracer, + check_nonce: true, + } + } +} + +impl TransactOptions { + /// Creates new `TransactOptions` with no tracing and default VM tracing. + pub fn with_vm_tracing() -> Self { + TransactOptions { + tracer: trace::NoopTracer, + vm_tracer: trace::ExecutiveVMTracer::toplevel(), + check_nonce: true, + } + } +} + +impl TransactOptions { + /// Creates new `TransactOptions` without any tracing. + pub fn with_no_tracing() -> Self { + TransactOptions { + tracer: trace::NoopTracer, + vm_tracer: trace::NoopVMTracer, + check_nonce: true, + } + } +} + pub fn executor(engine: &E, vm_factory: &Factory, params: &ActionParams) -> Box where E: Engine + ?Sized { @@ -137,24 +198,18 @@ impl<'a, B: 'a + StateBackend, E: Engine + ?Sized> Executive<'a, B, E> { } /// This function should be used to execute transaction. - pub fn transact(&'a mut self, t: &SignedTransaction, options: TransactOptions) -> Result { - let check = options.check_nonce; - match options.tracing { - true => match options.vm_tracing { - true => self.transact_with_tracer(t, check, ExecutiveTracer::default(), ExecutiveVMTracer::toplevel()), - false => self.transact_with_tracer(t, check, ExecutiveTracer::default(), NoopVMTracer), - }, - false => match options.vm_tracing { - true => self.transact_with_tracer(t, check, NoopTracer, ExecutiveVMTracer::toplevel()), - false => self.transact_with_tracer(t, check, NoopTracer, NoopVMTracer), - }, - } + pub fn transact(&'a mut self, t: &SignedTransaction, options: TransactOptions) + -> Result where T: Tracer, V: VMTracer, + { + self.transact_with_tracer(t, options.check_nonce, options.tracer, options.vm_tracer) } /// Execute a transaction in a "virtual" context. /// This will ensure the caller has enough balance to execute the desired transaction. /// Used for extra-block executions for things like consensus contracts and RPCs - pub fn transact_virtual(&'a mut self, t: &SignedTransaction, options: TransactOptions) -> Result { + pub fn transact_virtual(&'a mut self, t: &SignedTransaction, options: TransactOptions) + -> Result where T: Tracer, V: VMTracer, + { let sender = t.sender(); let balance = self.state.balance(&sender)?; let needed_balance = t.value.saturating_add(t.gas.saturating_mul(t.gas_price)); @@ -167,7 +222,7 @@ impl<'a, B: 'a + StateBackend, E: Engine + ?Sized> Executive<'a, B, E> { } /// Execute transaction/call with tracing enabled - pub fn transact_with_tracer( + fn transact_with_tracer( &'a mut self, t: &SignedTransaction, check_nonce: bool, @@ -261,7 +316,7 @@ impl<'a, B: 'a + StateBackend, E: Engine + ?Sized> Executive<'a, B, E> { }; // finalize here! - Ok(self.finalize(t, substate, result, output, tracer.traces(), vm_tracer.drain())?) + Ok(self.finalize(t, substate, result, output, tracer.drain(), vm_tracer.drain())?) } fn exec_vm( @@ -399,7 +454,7 @@ impl<'a, B: 'a + StateBackend, E: Engine + ?Sized> Executive<'a, B, E> { trace!(target: "executive", "res={:?}", res); - let traces = subtracer.traces(); + let traces = subtracer.drain(); match res { Ok(ref res) => tracer.trace_call( trace_info, @@ -484,9 +539,9 @@ impl<'a, B: 'a + StateBackend, E: Engine + ?Sized> Executive<'a, B, E> { gas - res.gas_left, trace_output, created, - subtracer.traces() + subtracer.drain() ), - Err(ref e) => tracer.trace_failed_create(trace_info, subtracer.traces(), e.into()) + Err(ref e) => tracer.trace_failed_create(trace_info, subtracer.drain(), e.into()) }; self.enact_result(&res, substate, unconfirmed_substate); @@ -794,7 +849,7 @@ mod tests { }), }]; - assert_eq!(tracer.traces(), expected_trace); + assert_eq!(tracer.drain(), expected_trace); let expected_vm_trace = VMTrace { parent_step: 0, @@ -887,7 +942,7 @@ mod tests { }), }]; - assert_eq!(tracer.traces(), expected_trace); + assert_eq!(tracer.drain(), expected_trace); let expected_vm_trace = VMTrace { parent_step: 0, @@ -1138,7 +1193,7 @@ mod tests { let executed = { let mut ex = Executive::new(&mut state, &info, &engine); - let opts = TransactOptions { check_nonce: true, tracing: false, vm_tracing: false }; + let opts = TransactOptions::with_no_tracing(); ex.transact(&t, opts).unwrap() }; @@ -1175,7 +1230,7 @@ mod tests { let res = { let mut ex = Executive::new(&mut state, &info, &engine); - let opts = TransactOptions { check_nonce: true, tracing: false, vm_tracing: false }; + let opts = TransactOptions::with_no_tracing(); ex.transact(&t, opts) }; @@ -1208,7 +1263,7 @@ mod tests { let res = { let mut ex = Executive::new(&mut state, &info, &engine); - let opts = TransactOptions { check_nonce: true, tracing: false, vm_tracing: false }; + let opts = TransactOptions::with_no_tracing(); ex.transact(&t, opts) }; @@ -1241,7 +1296,7 @@ mod tests { let res = { let mut ex = Executive::new(&mut state, &info, &engine); - let opts = TransactOptions { check_nonce: true, tracing: false, vm_tracing: false }; + let opts = TransactOptions::with_no_tracing(); ex.transact(&t, opts) }; diff --git a/ethcore/src/json_tests/state.rs b/ethcore/src/json_tests/state.rs index 2fdf8875f..51961a2bb 100644 --- a/ethcore/src/json_tests/state.rs +++ b/ethcore/src/json_tests/state.rs @@ -15,23 +15,13 @@ // along with Parity. If not, see . use super::test_common::*; -use tests::helpers::*; use pod_state::PodState; -use ethereum; -use spec::Spec; +use trace; +use client::{EvmTestClient, EvmTestError, TransactResult}; use ethjson; -use ethjson::state::test::ForkSpec; use transaction::SignedTransaction; use vm::EnvInfo; -lazy_static! { - pub static ref FRONTIER: Spec = ethereum::new_frontier_test(); - pub static ref HOMESTEAD: Spec = ethereum::new_homestead_test(); - pub static ref EIP150: Spec = ethereum::new_eip150_test(); - pub static ref EIP161: Spec = ethereum::new_eip161_test(); - pub static ref _METROPOLIS: Spec = ethereum::new_metropolis_test(); -} - pub fn json_chain_test(json_data: &[u8]) -> Vec { ::ethcore_logger::init_log(); let tests = ethjson::state::test::Test::load(json_data).unwrap(); @@ -43,35 +33,49 @@ pub fn json_chain_test(json_data: &[u8]) -> Vec { let env: EnvInfo = test.env.into(); let pre: PodState = test.pre_state.into(); - for (spec, states) in test.post_states { + for (spec_name, states) in test.post_states { let total = states.len(); - let engine = match spec { - ForkSpec::Frontier => &FRONTIER.engine, - ForkSpec::Homestead => &HOMESTEAD.engine, - ForkSpec::EIP150 => &EIP150.engine, - ForkSpec::EIP158 => &EIP161.engine, - ForkSpec::Metropolis => continue, + let spec = match EvmTestClient::spec_from_json(&spec_name) { + Some(spec) => spec, + None => { + println!(" - {} | {:?} Ignoring tests because of missing spec", name, spec_name); + continue; + } }; for (i, state) in states.into_iter().enumerate() { - let info = format!(" - {} | {:?} ({}/{}) ...", name, spec, i + 1, total); + let info = format!(" - {} | {:?} ({}/{}) ...", name, spec_name, i + 1, total); let post_root: H256 = state.hash.into(); let transaction: SignedTransaction = multitransaction.select(&state.indexes).into(); - let mut state = get_temp_state(); - state.populate_from(pre.clone()); - if transaction.verify_basic(true, None, env.number >= engine.params().eip86_transition).is_ok() { - state.commit().expect(&format!("State test {} failed due to internal error.", name)); - let _res = state.apply(&env, &**engine, &transaction, false); - } else { - let _rest = state.commit(); - } - if state.root() != &post_root { - println!("{} !!! State mismatch (got: {}, expect: {}", info, state.root(), post_root); - flushln!("{} fail", info); - failed.push(name.clone()); - } else { - flushln!("{} ok", info); + + let result = || -> Result<_, EvmTestError> { + Ok(EvmTestClient::from_pod_state(spec, pre.clone())? + .transact(&env, transaction, trace::NoopVMTracer)) + }; + match result() { + Err(err) => { + println!("{} !!! Unexpected internal error: {:?}", info, err); + flushln!("{} fail", info); + failed.push(name.clone()); + }, + Ok(TransactResult::Ok { state_root, .. }) if state_root != post_root => { + println!("{} !!! State mismatch (got: {}, expect: {}", info, state_root, post_root); + flushln!("{} fail", info); + failed.push(name.clone()); + }, + Ok(TransactResult::Err { state_root, ref error }) if state_root != post_root => { + println!("{} !!! State mismatch (got: {}, expect: {}", info, state_root, post_root); + println!("{} !!! Execution error: {:?}", info, error); + flushln!("{} fail", info); + failed.push(name.clone()); + }, + Ok(TransactResult::Err { error, .. }) => { + flushln!("{} ok ({:?})", info, error); + }, + Ok(_) => { + flushln!("{} ok", info); + }, } } } diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index ad884d91b..1b36c91ec 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -31,7 +31,7 @@ use vm::EnvInfo; use error::Error; use executive::{Executive, TransactOptions}; use factory::Factories; -use trace::FlatTrace; +use trace::{self, FlatTrace, VMTrace}; use pod_account::*; use pod_state::{self, PodState}; use types::basic_account::BasicAccount; @@ -59,8 +59,12 @@ pub use self::substate::Substate; pub struct ApplyOutcome { /// The receipt for the applied transaction. pub receipt: Receipt, - /// The trace for the applied transaction, if None if tracing is disabled. + /// The output of the applied transaction. + pub output: Bytes, + /// The trace for the applied transaction, empty if tracing was not produced. pub trace: Vec, + /// The VM trace for the applied transaction, None if tracing was not produced. + pub vm_trace: Option } /// Result type for the execution ("application") of a transaction. @@ -205,7 +209,7 @@ pub fn check_proof( Err(_) => return ProvedExecution::BadProof, }; - match state.execute(env_info, engine, transaction, false, true) { + match state.execute(env_info, engine, transaction, TransactOptions::with_no_tracing(), true) { Ok(executed) => ProvedExecution::Complete(executed), Err(ExecutionError::Internal(_)) => ProvedExecution::BadProof, Err(e) => ProvedExecution::Failed(e), @@ -290,7 +294,7 @@ const SEC_TRIE_DB_UNWRAP_STR: &'static str = "A state can only be created with v impl State { /// Creates new state with empty state root - #[cfg(test)] + /// Used for tests. pub fn new(mut db: B, account_start_nonce: U256, factories: Factories) -> State { let mut root = H256::new(); { @@ -623,29 +627,57 @@ impl State { /// Execute a given transaction, producing a receipt and an optional trace. /// This will change the state accordingly. pub fn apply(&mut self, env_info: &EnvInfo, engine: &Engine, t: &SignedTransaction, tracing: bool) -> ApplyResult { -// let old = self.to_pod(); + if tracing { + let options = TransactOptions::with_tracing(); + self.apply_with_tracing(env_info, engine, t, options.tracer, options.vm_tracer) + } else { + let options = TransactOptions::with_no_tracing(); + self.apply_with_tracing(env_info, engine, t, options.tracer, options.vm_tracer) + } + } + + /// Execute a given transaction with given tracer and VM tracer producing a receipt and an optional trace. + /// This will change the state accordingly. + pub fn apply_with_tracing( + &mut self, + env_info: &EnvInfo, + engine: &Engine, + t: &SignedTransaction, + tracer: T, + vm_tracer: V, + ) -> ApplyResult where + T: trace::Tracer, + V: trace::VMTracer, + { + let options = TransactOptions::new(tracer, vm_tracer); + let e = self.execute(env_info, engine, t, options, false)?; - let e = self.execute(env_info, engine, t, tracing, false)?; -// trace!("Applied transaction. Diff:\n{}\n", state_diff::diff_pod(&old, &self.to_pod())); let state_root = if env_info.number < engine.params().eip98_transition || env_info.number < engine.params().validate_receipts_transition { self.commit()?; Some(self.root().clone()) } else { None }; + + let output = e.output; let receipt = Receipt::new(state_root, e.cumulative_gas_used, e.logs); trace!(target: "state", "Transaction receipt: {:?}", receipt); - Ok(ApplyOutcome{receipt: receipt, trace: e.trace}) + + Ok(ApplyOutcome { + receipt, + output, + trace: e.trace, + vm_trace: e.vm_trace, + }) } // Execute a given transaction without committing changes. // // `virt` signals that we are executing outside of a block set and restrictions like // gas limits and gas costs should be lifted. - fn execute(&mut self, env_info: &EnvInfo, engine: &Engine, t: &SignedTransaction, tracing: bool, virt: bool) - -> Result + fn execute(&mut self, env_info: &EnvInfo, engine: &Engine, t: &SignedTransaction, options: TransactOptions, virt: bool) + -> Result where T: trace::Tracer, V: trace::VMTracer, { - let options = TransactOptions { tracing: tracing, vm_tracing: false, check_nonce: true }; let mut e = Executive::new(self, env_info, engine); match virt { @@ -730,9 +762,8 @@ impl State { Ok(()) } - #[cfg(test)] - #[cfg(feature = "json-tests")] /// Populate the state from `accounts`. + /// Used for tests. pub fn populate_from(&mut self, accounts: PodState) { assert!(self.checkpoints.borrow().is_empty()); for (add, acc) in accounts.drain().into_iter() { diff --git a/ethcore/src/tests/client.rs b/ethcore/src/tests/client.rs index 639fce3ab..ea7dd32f5 100644 --- a/ethcore/src/tests/client.rs +++ b/ethcore/src/tests/client.rs @@ -19,7 +19,7 @@ use std::sync::Arc; use io::IoChannel; use client::{BlockChainClient, MiningBlockChainClient, Client, ClientConfig, BlockId}; use state::{self, State, CleanupMode}; -use executive::Executive; +use executive::{Executive, TransactOptions}; use ethereum; use block::IsBlock; use tests::helpers::*; @@ -361,7 +361,7 @@ fn transaction_proof() { let mut state = State::from_existing(backend, root, 0.into(), factories.clone()).unwrap(); Executive::new(&mut state, &client.latest_env_info(), &*test_spec.engine) - .transact(&transaction, Default::default()).unwrap(); + .transact(&transaction, TransactOptions::with_no_tracing().dont_check_nonce()).unwrap(); assert_eq!(state.balance(&Address::default()).unwrap(), 5.into()); assert_eq!(state.balance(&address).unwrap(), 95.into()); diff --git a/ethcore/src/trace/executive_tracer.rs b/ethcore/src/trace/executive_tracer.rs index 860c74223..ba4d0eff9 100644 --- a/ethcore/src/trace/executive_tracer.rs +++ b/ethcore/src/trace/executive_tracer.rs @@ -167,7 +167,7 @@ impl Tracer for ExecutiveTracer { ExecutiveTracer::default() } - fn traces(self) -> Vec { + fn drain(self) -> Vec { self.traces } } diff --git a/ethcore/src/trace/mod.rs b/ethcore/src/trace/mod.rs index be830430b..c749bfd82 100644 --- a/ethcore/src/trace/mod.rs +++ b/ethcore/src/trace/mod.rs @@ -85,7 +85,7 @@ pub trait Tracer: Send { fn subtracer(&self) -> Self where Self: Sized; /// Consumes self and returns all traces. - fn traces(self) -> Vec; + fn drain(self) -> Vec; } /// Used by executive to build VM traces. diff --git a/ethcore/src/trace/noop_tracer.rs b/ethcore/src/trace/noop_tracer.rs index 2c0e1b830..03d6f57a0 100644 --- a/ethcore/src/trace/noop_tracer.rs +++ b/ethcore/src/trace/noop_tracer.rs @@ -62,7 +62,7 @@ impl Tracer for NoopTracer { NoopTracer } - fn traces(self) -> Vec { + fn drain(self) -> Vec { vec![] } } diff --git a/evmbin/Cargo.toml b/evmbin/Cargo.toml index 698646a3c..5538e10cc 100644 --- a/evmbin/Cargo.toml +++ b/evmbin/Cargo.toml @@ -14,6 +14,7 @@ docopt = "0.8" serde = "1.0" serde_derive = "1.0" ethcore = { path = "../ethcore" } +ethjson = { path = "../json" } ethcore-util = { path = "../util" } evm = { path = "../ethcore/evm" } vm = { path = "../ethcore/vm" } diff --git a/evmbin/src/display/json.rs b/evmbin/src/display/json.rs index e259eec7a..39147ffa3 100644 --- a/evmbin/src/display/json.rs +++ b/evmbin/src/display/json.rs @@ -30,10 +30,8 @@ pub struct Informant { depth: usize, pc: usize, instruction: u8, - name: &'static str, gas_cost: U256, gas_used: U256, - stack_pop: usize, stack: Vec, memory: Vec, storage: HashMap, @@ -58,11 +56,19 @@ impl Informant { } impl vm::Informant for Informant { + fn before_test(&self, name: &str, action: &str) { + println!( + "{{\"test\":\"{name}\",\"action\":\"{action}\"}}", + name = name, + action = action, + ); + } + fn set_gas(&mut self, gas: U256) { self.gas_used = gas; } - fn finish(&mut self, result: Result) { + fn finish(result: Result) { match result { Ok(success) => println!( "{{\"output\":\"0x{output}\",\"gasUsed\":\"{gas:x}\",\"time\":{time}}}", @@ -112,7 +118,7 @@ impl trace::VMTracer for Informant { self.gas_used = gas_used; let len = self.stack.len(); - self.stack.truncate(len - info.args); + self.stack.truncate(if len > info.args { len - info.args } else { 0 }); self.stack.extend_from_slice(stack_push); if let Some((pos, data)) = mem_diff { diff --git a/evmbin/src/display/simple.rs b/evmbin/src/display/simple.rs index b03f97c8d..bb4ecc127 100644 --- a/evmbin/src/display/simple.rs +++ b/evmbin/src/display/simple.rs @@ -27,7 +27,11 @@ use info as vm; pub struct Informant; impl vm::Informant for Informant { - fn finish(&mut self, result: Result) { + fn before_test(&self, name: &str, action: &str) { + println!("Test: {} ({})", name, action); + } + + fn finish(result: Result) { match result { Ok(success) => { println!("Output: 0x{}", success.output.to_hex()); diff --git a/evmbin/src/info.rs b/evmbin/src/info.rs index 617ebc7b9..3392cb441 100644 --- a/evmbin/src/info.rs +++ b/evmbin/src/info.rs @@ -17,17 +17,19 @@ //! VM runner. use std::time::{Instant, Duration}; -use util::U256; -use ethcore::{trace, spec}; -use ethcore::client::{EvmTestClient, EvmTestError}; -use vm::ActionParams; +use util::{U256, H256}; +use ethcore::{trace, spec, transaction, pod_state}; +use ethcore::client::{self, EvmTestClient, EvmTestError, TransactResult}; +use ethjson; /// VM execution informant pub trait Informant: trace::VMTracer { + /// Display a single run init message + fn before_test(&self, test: &str, action: &str); /// Set initial gas. fn set_gas(&mut self, _gas: U256) {} /// Display final result. - fn finish(&mut self, result: Result); + fn finish(result: Result); } /// Execution finished correctly @@ -50,17 +52,71 @@ pub struct Failure { pub time: Duration, } +/// Execute given Transaction and verify resulting state root. +pub fn run_transaction( + name: &str, + idx: usize, + spec: ðjson::state::test::ForkSpec, + pre_state: &pod_state::PodState, + post_root: H256, + env_info: &client::EnvInfo, + transaction: transaction::SignedTransaction, + mut informant: T, +) { + let spec_name = format!("{:?}", spec).to_lowercase(); + let spec = match EvmTestClient::spec_from_json(spec) { + Some(spec) => { + informant.before_test(&format!("{}:{}:{}", name, spec_name, idx), "starting"); + spec + }, + None => { + informant.before_test(&format!("{}:{}:{}", name, spec_name, idx), "skipping because of missing spec"); + return; + }, + }; + + informant.set_gas(env_info.gas_limit); + + let result = run(spec, env_info.gas_limit, pre_state, |mut client| { + let result = client.transact(env_info, transaction, informant); + match result { + TransactResult::Ok { state_root, .. } if state_root != post_root => { + Err(EvmTestError::PostCondition(format!( + "State root mismatch (got: {}, expected: {})", + state_root, + post_root, + ))) + }, + TransactResult::Ok { gas_left, output, .. } => { + Ok((gas_left, output)) + }, + TransactResult::Err { error, .. } => { + Err(EvmTestError::PostCondition(format!( + "Unexpected execution error: {:?}", error + ))) + }, + } + }); + + T::finish(result) +} + /// Execute VM with given `ActionParams` -pub fn run(vm_tracer: &mut T, spec: spec::Spec, params: ActionParams) -> Result { - let mut test_client = EvmTestClient::new(spec).map_err(|error| Failure { +pub fn run<'a, F, T>(spec: &'a spec::Spec, initial_gas: U256, pre_state: T, run: F) -> Result where + F: FnOnce(EvmTestClient) -> Result<(U256, Vec), EvmTestError>, + T: Into>, +{ + let test_client = match pre_state.into() { + Some(pre_state) => EvmTestClient::from_pod_state(spec, pre_state.clone()), + None => EvmTestClient::new(spec), + }.map_err(|error| Failure { gas_used: 0.into(), error, time: Duration::from_secs(0) })?; - let initial_gas = params.gas; let start = Instant::now(); - let result = test_client.call(params, vm_tracer); + let result = run(test_client); let duration = start.elapsed(); match result { diff --git a/evmbin/src/main.rs b/evmbin/src/main.rs index bad0ef1f0..5eb43ed61 100644 --- a/evmbin/src/main.rs +++ b/evmbin/src/main.rs @@ -17,8 +17,9 @@ //! Parity EVM interpreter binary. #![warn(missing_docs)] -#![allow(dead_code)] + extern crate ethcore; +extern crate ethjson; extern crate rustc_hex; extern crate serde; #[macro_use] @@ -31,6 +32,7 @@ extern crate panic_hook; use std::sync::Arc; use std::{fmt, fs}; +use std::path::PathBuf; use docopt::Docopt; use rustc_hex::FromHex; use util::{U256, Bytes, Address}; @@ -47,6 +49,7 @@ EVM implementation for Parity. Copyright 2016, 2017 Parity Technologies (UK) Ltd Usage: + parity-evm state-test [--json --only NAME --chain CHAIN] parity-evm stats [options] parity-evm [options] parity-evm [-h | --help] @@ -59,6 +62,10 @@ Transaction options: --gas GAS Supplied gas as hex (without 0x). --gas-price WEI Supplied gas price as hex (without 0x). +State test options: + --only NAME Runs only a single test matching the name. + --chain CHAIN Run only tests from specific chain. + General options: --json Display verbose results in JSON. --chain CHAIN Chain spec file path. @@ -71,20 +78,67 @@ fn main() { let args: Args = Docopt::new(USAGE).and_then(|d| d.deserialize()).unwrap_or_else(|e| e.exit()); - if args.flag_json { - run(args, display::json::Informant::default()) + if args.cmd_state_test { + run_state_test(args) + } else if args.flag_json { + run_call(args, display::json::Informant::default()) } else { - run(args, display::simple::Informant::default()) + run_call(args, display::simple::Informant::default()) } } -fn run(args: Args, mut informant: T) { +fn run_state_test(args: Args) { + use ethjson::state::test::Test; + + let file = args.arg_file.expect("FILE is required"); + let mut file = match fs::File::open(&file) { + Err(err) => die(format!("Unable to open: {:?}: {}", file, err)), + Ok(file) => file, + }; + let state_test = match Test::load(&mut file) { + Err(err) => die(format!("Unable to load the test file: {}", err)), + Ok(test) => test, + }; + let only_test = args.flag_only.map(|s| s.to_lowercase()); + let only_chain = args.flag_chain.map(|s| s.to_lowercase()); + + for (name, test) in state_test { + if let Some(false) = only_test.as_ref().map(|only_test| &name.to_lowercase() == only_test) { + continue; + } + + let multitransaction = test.transaction; + let env_info = test.env.into(); + let pre = test.pre_state.into(); + + for (spec, states) in test.post_states { + if let Some(false) = only_chain.as_ref().map(|only_chain| &format!("{:?}", spec).to_lowercase() == only_chain) { + continue; + } + + for (idx, state) in states.into_iter().enumerate() { + let post_root = state.hash.into(); + let transaction = multitransaction.select(&state.indexes).into(); + + if args.flag_json { + let i = display::json::Informant::default(); + info::run_transaction(&name, idx, &spec, &pre, post_root, &env_info, transaction, i) + } else { + let i = display::simple::Informant::default(); + info::run_transaction(&name, idx, &spec, &pre, post_root, &env_info, transaction, i) + } + } + } + } +} + +fn run_call(args: Args, mut informant: T) { let from = arg(args.from(), "--from"); let to = arg(args.to(), "--to"); let code = arg(args.code(), "--code"); let spec = arg(args.spec(), "--chain"); let gas = arg(args.gas(), "--gas"); - let gas_price = arg(args.gas(), "--gas-price"); + let gas_price = arg(args.gas_price(), "--gas-price"); let data = arg(args.data(), "--input"); if code.is_none() && to == Address::default() { @@ -103,13 +157,18 @@ fn run(args: Args, mut informant: T) { params.data = data; informant.set_gas(gas); - let result = info::run(&mut informant, spec, params); - informant.finish(result); + let result = info::run(&spec, gas, None, |mut client| { + client.call(params, &mut informant) + }); + T::finish(result); } #[derive(Debug, Deserialize)] struct Args { cmd_stats: bool, + cmd_state_test: bool, + arg_file: Option, + flag_only: Option, flag_from: Option, flag_to: Option, flag_code: Option, @@ -221,4 +280,22 @@ mod tests { assert_eq!(args.data(), Ok(Some(vec![06]))); assert_eq!(args.flag_chain, Some("./testfile".to_owned())); } + + #[test] + fn should_parse_state_test_command() { + let args = run(&[ + "parity-evm", + "state-test", + "./file.json", + "--chain", "homestead", + "--only=add11", + "--json", + ]); + + assert_eq!(args.cmd_state_test, true); + assert!(args.arg_file.is_some()); + assert_eq!(args.flag_json, true); + assert_eq!(args.flag_chain, Some("homestead".to_owned())); + assert_eq!(args.flag_only, Some("add11".to_owned())); + } } diff --git a/json/src/state/test.rs b/json/src/state/test.rs index ceaccfd17..a63b8ee8d 100644 --- a/json/src/state/test.rs +++ b/json/src/state/test.rs @@ -104,7 +104,10 @@ pub enum ForkSpec { EIP158, Frontier, Homestead, + // TODO [ToDr] Deprecated Metropolis, + Byzantium, + Constantinople, } /// State test indexes deserialization. @@ -161,7 +164,7 @@ mod tests { "EIP150" : [ { "hash" : "3e6dacc1575c6a8c76422255eca03529bbf4c0dda75dfc110b22d6dc4152396f", - "indexes" : { "data" : 0, "gas" : 0, "value" : 0 } + "indexes" : { "data" : 0, "gas" : 0, "value" : 0 } }, { "hash" : "99a450d8ce5b987a71346d8a0a1203711f770745c7ef326912e46761f14cd764",