From 832c4a7565cfefd75136469f8aa41a3203e6d103 Mon Sep 17 00:00:00 2001 From: cheme Date: Sun, 25 Nov 2018 20:12:59 +0100 Subject: [PATCH] Add a optional json dump state to evm-bin (#9706) * Add a dump of the state at the end of transaction for --json-test. Also fixes json-test output on finish, and allow to put both on err or out (--out-only and --err-only). * Dump state resolution from trie, function behind evm-bin feature to avoid misuse. * Rename 'slow' method to 'to_pod_full'. Use cache first in 'to_pod_full', for in between commits case. Change dump activation to use a function pointer instead. * Fix tests. * Query and add storage values to dump. * Switch to use `require` method, even if less efficient it is better in this case to reuse existing code. Reuse of `storage_at` was not easy in this case (could not iterate and use the method at the same time (refcell mutable borrow panics) so keeping code as is. * Switch to returning error. Use 'base_storage_root' instead of 'storage_root'. Added a test, it will only execute with json-test in ci, or when launch with the feature. * Renaming of command line parameters. Comments fixes. Minor code changes. * Fix evmbin cmd parsing test. * README update. * Fix extra space and avoid clone call on copiable address. * Revert test submodule. * Revert wasm-test submodule. * Use map_or instead of map + unwrap_or * restore tests submodule --- Cargo.lock | 2 + ethcore/Cargo.toml | 6 +- ethcore/src/client/evm_test_client.rs | 56 ++++++++-- ethcore/src/json_tests/state.rs | 2 +- ethcore/src/lib.rs | 4 + ethcore/src/pod_account.rs | 11 +- ethcore/src/pod_state.rs | 4 +- ethcore/src/state/account.rs | 2 +- ethcore/src/state/mod.rs | 154 +++++++++++++++++++++----- ethcore/src/trace/mod.rs | 1 + ethcore/wasm/run/src/runner.rs | 2 +- evmbin/Cargo.toml | 2 +- evmbin/README.md | 7 +- evmbin/src/display/json.rs | 6 +- evmbin/src/display/simple.rs | 7 +- evmbin/src/display/std_json.rs | 54 +++++++-- evmbin/src/info.rs | 78 +++++++++---- evmbin/src/main.rs | 67 ++++++++--- 18 files changed, 369 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7cccfb18..623a5967e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,6 +685,8 @@ dependencies = [ "rlp_compress 0.1.0", "rlp_derive 0.1.0", "rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.78 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.78 (registry+https://github.com/rust-lang/crates.io-index)", "stats 0.1.0", "stop-guard 0.1.0", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/ethcore/Cargo.toml b/ethcore/Cargo.toml index 0e3ea23a7..fc034a796 100644 --- a/ethcore/Cargo.toml +++ b/ethcore/Cargo.toml @@ -67,6 +67,8 @@ unexpected = { path = "../util/unexpected" } journaldb = { path = "../util/journaldb" } keccak-hasher = { path = "../util/keccak-hasher" } kvdb-rocksdb = "0.1.3" +serde = "1.0" +serde_derive = "1.0" tempdir = {version="0.3", optional = true} [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "android"))'.dependencies] @@ -103,13 +105,15 @@ evm-debug-tests = ["evm-debug", "evm/evm-debug-tests"] # EVM debug traces are printed. slow-blocks = [] # Run JSON consensus tests. -json-tests = ["ethcore-transaction/json-tests", "test-helpers", "tempdir"] +json-tests = ["ethcore-transaction/json-tests", "test-helpers", "tempdir", "to-pod-full"] # Skip JSON consensus tests with pending issues. ci-skip-issue = [] # Run memory/cpu heavy tests. test-heavy = [] # Compile test helpers test-helpers = ["tempdir"] +# Enables slow 'to-pod-full' method for use in tests and evmbin. +to-pod-full = [] [[bench]] name = "builtin" diff --git a/ethcore/src/client/evm_test_client.rs b/ethcore/src/client/evm_test_client.rs index 20a04613a..67e187244 100644 --- a/ethcore/src/client/evm_test_client.rs +++ b/ethcore/src/client/evm_test_client.rs @@ -66,6 +66,11 @@ use ethjson::spec::ForkSpec; pub struct EvmTestClient<'a> { state: state::State, spec: &'a spec::Spec, + dump_state: fn(&state::State) -> Option, +} + +fn no_dump_state(_: &state::State) -> Option { + None } impl<'a> fmt::Debug for EvmTestClient<'a> { @@ -92,32 +97,51 @@ impl<'a> EvmTestClient<'a> { } } + /// Change default function for dump state (default does not dump) + pub fn set_dump_state_fn(&mut self, dump_state: fn(&state::State) -> Option) { + self.dump_state = dump_state; + } + /// 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(); + /// Takes a `TrieSpec` to set the type of trie. + pub fn new_with_trie(spec: &'a spec::Spec, trie_spec: trie::TrieSpec) -> Result { + let factories = Self::factories(trie_spec); let state = Self::state_from_spec(spec, &factories)?; Ok(EvmTestClient { state, spec, + dump_state: no_dump_state, }) } - /// 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(); + /// Creates new EVM test client with an in-memory DB initialized with genesis of given chain Spec. + pub fn new(spec: &'a spec::Spec) -> Result { + Self::new_with_trie(spec, trie::TrieSpec::Secure) + } + + /// Creates new EVM test client with an in-memory DB initialized with given PodState. + /// Takes a `TrieSpec` to set the type of trie. + pub fn from_pod_state_with_trie(spec: &'a spec::Spec, pod_state: pod_state::PodState, trie_spec: trie::TrieSpec) -> Result { + let factories = Self::factories(trie_spec); let state = Self::state_from_pod(spec, &factories, pod_state)?; Ok(EvmTestClient { state, spec, + dump_state: no_dump_state, }) } - fn factories() -> Factories { + /// Creates new EVM test client with an in-memory DB initialized with given PodState. + pub fn from_pod_state(spec: &'a spec::Spec, pod_state: pod_state::PodState) -> Result { + Self::from_pod_state_with_trie(spec, pod_state, trie::TrieSpec::Secure) + } + + fn factories(trie_spec: trie::TrieSpec) -> Factories { Factories { vm: factory::VmFactory::new(VMType::Interpreter, 5 * 1024), - trie: trie::TrieFactory::new(trie::TrieSpec::Secure), + trie: trie::TrieFactory::new(trie_spec), accountdb: Default::default(), } } @@ -223,6 +247,7 @@ impl<'a> EvmTestClient<'a> { return TransactResult::Err { state_root: *self.state.root(), error: error.into(), + end_state: (self.dump_state)(&self.state), }; } @@ -247,12 +272,17 @@ impl<'a> EvmTestClient<'a> { &None, false ).ok(); + self.state.commit().ok(); + let state_root = *self.state.root(); + + let end_state = (self.dump_state)(&self.state); + match result { Ok(result) => { TransactResult::Ok { - state_root: *self.state.root(), + state_root, gas_left: initial_gas - result.receipt.gas_used, outcome: result.receipt.outcome, output: result.output, @@ -263,12 +293,14 @@ impl<'a> EvmTestClient<'a> { Some(executive::contract_address(scheme, &transaction.sender(), &transaction.nonce, &transaction.data).0) } else { None - } + }, + end_state, } }, Err(error) => TransactResult::Err { - state_root: *self.state.root(), + state_root, error, + end_state, }, } } @@ -295,6 +327,8 @@ pub enum TransactResult { logs: Vec, /// outcome outcome: receipt::TransactionOutcome, + /// end state if needed + end_state: Option, }, /// Transaction failed to run Err { @@ -302,5 +336,7 @@ pub enum TransactResult { state_root: H256, /// Execution error error: ::error::Error, + /// end state if needed + end_state: Option, }, } diff --git a/ethcore/src/json_tests/state.rs b/ethcore/src/json_tests/state.rs index f402a7b37..733e26802 100644 --- a/ethcore/src/json_tests/state.rs +++ b/ethcore/src/json_tests/state.rs @@ -95,7 +95,7 @@ pub fn json_chain_test(json_data: &[u8], start_stop_ho flushln!("{} fail", info); failed.push(name.clone()); }, - Ok(TransactResult::Err { state_root, ref error }) if state_root != post_root => { + 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); diff --git a/ethcore/src/lib.rs b/ethcore/src/lib.rs index 9a0315220..b3d77cbfa 100644 --- a/ethcore/src/lib.rs +++ b/ethcore/src/lib.rs @@ -109,6 +109,7 @@ extern crate vm; extern crate wasm; extern crate memory_cache; extern crate journaldb; +extern crate serde; #[cfg(any(test, feature = "json-tests", feature = "test-helpers"))] extern crate tempdir; @@ -134,6 +135,8 @@ extern crate macros; extern crate rlp_derive; #[macro_use] extern crate trace_time; +#[macro_use] +extern crate serde_derive; #[cfg_attr(test, macro_use)] extern crate evm; @@ -187,3 +190,4 @@ pub use types::*; pub use executive::contract_address; pub use evm::CreateContractAddress; pub use blockchain::{BlockChainDB, BlockChainDBHandler}; +pub use trie::TrieSpec; diff --git a/ethcore/src/pod_account.rs b/ethcore/src/pod_account.rs index db5d0660c..c4d05cca5 100644 --- a/ethcore/src/pod_account.rs +++ b/ethcore/src/pod_account.rs @@ -32,8 +32,10 @@ use state::Account; use ethjson; use types::account_diff::*; use rlp::{self, RlpStream}; +use serde::Serializer; +use rustc_hex::ToHex; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] /// An account, expressed as Plain-Old-Data (hence the name). /// Does not have a DB overlay cache, code hash or anything like that. pub struct PodAccount { @@ -41,12 +43,19 @@ pub struct PodAccount { pub balance: U256, /// The nonce of the account. pub nonce: U256, + #[serde(serialize_with="opt_bytes_to_hex")] /// The code of the account or `None` in the special case that it is unknown. pub code: Option, /// The storage of the account. pub storage: BTreeMap, } +fn opt_bytes_to_hex(opt_bytes: &Option, serializer: S) -> Result + where S: Serializer +{ + serializer.collect_str(&format_args!("0x{}",opt_bytes.as_ref().map_or("".to_string(), |b|b.to_hex()))) +} + impl PodAccount { /// Convert Account to a PodAccount. /// NOTE: This will silently fail unless the account is fully cached. diff --git a/ethcore/src/pod_state.rs b/ethcore/src/pod_state.rs index 406abffb7..76d6495c5 100644 --- a/ethcore/src/pod_state.rs +++ b/ethcore/src/pod_state.rs @@ -26,8 +26,8 @@ use types::state_diff::StateDiff; use ethjson; /// State of all accounts in the system expressed in Plain Old Data. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct PodState (BTreeMap); +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct PodState(BTreeMap); impl PodState { /// Contruct a new object from the `m`. diff --git a/ethcore/src/state/account.rs b/ethcore/src/state/account.rs index 274366b6d..912547877 100644 --- a/ethcore/src/state/account.rs +++ b/ethcore/src/state/account.rs @@ -624,7 +624,7 @@ mod tests { assert!(raw.len() > compact_vec.len()); let again_raw = decompress(&compact_vec, snapshot_swapper()); assert_eq!(raw, again_raw.into_vec()); - } + } #[test] fn storage_at() { diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index acb668ac7..535a77938 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -947,20 +947,78 @@ impl State { } /// Populate a PodAccount map from this state. - pub fn to_pod(&self) -> PodState { + fn to_pod_cache(&self) -> PodState { assert!(self.checkpoints.borrow().is_empty()); - // TODO: handle database rather than just the cache. - // will need fat db. PodState::from(self.cache.borrow().iter().fold(BTreeMap::new(), |mut m, (add, opt)| { if let Some(ref acc) = opt.account { - m.insert(add.clone(), PodAccount::from_account(acc)); + m.insert(*add, PodAccount::from_account(acc)); } m })) } + #[cfg(feature="to-pod-full")] + /// Populate a PodAccount map from this state. + /// Warning this is not for real time use. + /// Use of this method requires FatDB mode to be able + /// to iterate on accounts. + pub fn to_pod_full(&self) -> Result { + + assert!(self.checkpoints.borrow().is_empty()); + assert!(self.factories.trie.is_fat()); + + let mut result = BTreeMap::new(); + + let trie = self.factories.trie.readonly(self.db.as_hashdb(), &self.root)?; + + // put trie in cache + for item in trie.iter()? { + if let Ok((addr, _dbval)) = item { + let address = Address::from_slice(&addr); + let _ = self.require(&address, true); + } + } + + // Resolve missing part + for (add, opt) in self.cache.borrow().iter() { + if let Some(ref acc) = opt.account { + let pod_account = self.account_to_pod_account(acc, add)?; + result.insert(add.clone(), pod_account); + } + } + + Ok(PodState::from(result)) + } + + /// Create a PodAccount from an account. + /// Differs from existing method by including all storage + /// values of the account to the PodAccount. + /// This function is only intended for use in small tests or with fresh accounts. + /// It requires FatDB. + #[cfg(feature="to-pod-full")] + fn account_to_pod_account(&self, account: &Account, address: &Address) -> Result { + let mut pod_storage = BTreeMap::new(); + let addr_hash = account.address_hash(address); + let accountdb = self.factories.accountdb.readonly(self.db.as_hashdb(), addr_hash); + let root = account.base_storage_root(); + + let trie = self.factories.trie.readonly(accountdb.as_hashdb(), &root)?; + for o_kv in trie.iter()? { + if let Ok((key, val)) = o_kv { + pod_storage.insert(key[..].into(), U256::from(&val[..]).into()); + } + } + + let mut pod_account = PodAccount::from_account(&account); + // cached one first + pod_storage.append(&mut pod_account.storage); + pod_account.storage = pod_storage; + Ok(pod_account) + } + + /// Populate a PodAccount map from this state, with another state as the account and storage query. - pub fn to_pod_diff(&mut self, query: &State) -> TrieResult { + fn to_pod_diff(&mut self, query: &State) -> TrieResult { assert!(self.checkpoints.borrow().is_empty()); // Merge PodAccount::to_pod for cache of self and `query`. @@ -1015,7 +1073,7 @@ impl State { /// Returns a `StateDiff` describing the difference from `orig` to `self`. /// Consumes self. pub fn diff_from(&self, mut orig: State) -> TrieResult { - let pod_state_post = self.to_pod(); + let pod_state_post = self.to_pod_cache(); let pod_state_pre = orig.to_pod_diff(self)?; Ok(pod_state::diff_pod(&pod_state_pre, &pod_state_post)) } @@ -2593,12 +2651,12 @@ mod tests { assert_eq!(diff_map.len(), 1); assert!(diff_map.get(&a).is_some()); assert_eq!(diff_map.get(&a), - pod_account::diff_pod(Some(&PodAccount { - balance: U256::from(100), - nonce: U256::zero(), - code: Some(Default::default()), - storage: Default::default() - }), None).as_ref()); + pod_account::diff_pod(Some(&PodAccount { + balance: U256::from(100), + nonce: U256::zero(), + code: Some(Default::default()), + storage: Default::default() + }), None).as_ref()); } #[test] @@ -2624,18 +2682,64 @@ mod tests { assert_eq!(diff_map.len(), 1); assert!(diff_map.get(&a).is_some()); assert_eq!(diff_map.get(&a), - pod_account::diff_pod(Some(&PodAccount { - balance: U256::zero(), - nonce: U256::zero(), - code: Some(Default::default()), - storage: vec![(H256::from(&U256::from(1u64)), H256::from(&U256::from(20u64)))] - .into_iter().collect(), - }), Some(&PodAccount { - balance: U256::zero(), - nonce: U256::zero(), - code: Some(Default::default()), - storage: vec![(H256::from(&U256::from(1u64)), H256::from(&U256::from(100u64)))] - .into_iter().collect(), - })).as_ref()); + pod_account::diff_pod(Some(&PodAccount { + balance: U256::zero(), + nonce: U256::zero(), + code: Some(Default::default()), + storage: vec![(H256::from(&U256::from(1u64)), H256::from(&U256::from(20u64)))] + .into_iter().collect(), + }), Some(&PodAccount { + balance: U256::zero(), + nonce: U256::zero(), + code: Some(Default::default()), + storage: vec![(H256::from(&U256::from(1u64)), H256::from(&U256::from(100u64)))] + .into_iter().collect(), + })).as_ref()); } + + #[cfg(feature="to-pod-full")] + #[test] + fn should_get_full_pod_storage_values() { + use trie::{TrieFactory, TrieSpec}; + + let a = 10.into(); + let db = get_temp_state_db(); + + let factories = Factories { + vm: Default::default(), + trie: TrieFactory::new(TrieSpec::Fat), + accountdb: Default::default(), + }; + + let get_pod_state_val = |pod_state : &PodState, ak, k| { + pod_state.get().get(ak).unwrap().storage.get(&k).unwrap().clone() + }; + + let storage_address = H256::from(&U256::from(1u64)); + + let (root, db) = { + let mut state = State::new(db, U256::from(0), factories.clone()); + state.set_storage(&a, storage_address.clone(), H256::from(&U256::from(20u64))).unwrap(); + let dump = state.to_pod_full().unwrap(); + assert_eq!(get_pod_state_val(&dump, &a, storage_address.clone()), H256::from(&U256::from(20u64))); + state.commit().unwrap(); + let dump = state.to_pod_full().unwrap(); + assert_eq!(get_pod_state_val(&dump, &a, storage_address.clone()), H256::from(&U256::from(20u64))); + state.drop() + }; + + let mut state = State::from_existing(db, root, U256::from(0u8), factories).unwrap(); + let dump = state.to_pod_full().unwrap(); + assert_eq!(get_pod_state_val(&dump, &a, storage_address.clone()), H256::from(&U256::from(20u64))); + state.set_storage(&a, storage_address.clone(), H256::from(&U256::from(21u64))).unwrap(); + let dump = state.to_pod_full().unwrap(); + assert_eq!(get_pod_state_val(&dump, &a, storage_address.clone()), H256::from(&U256::from(21u64))); + state.commit().unwrap(); + state.set_storage(&a, storage_address.clone(), H256::from(&U256::from(0u64))).unwrap(); + let dump = state.to_pod_full().unwrap(); + assert_eq!(get_pod_state_val(&dump, &a, storage_address.clone()), H256::from(&U256::from(0u64))); + + + } + } diff --git a/ethcore/src/trace/mod.rs b/ethcore/src/trace/mod.rs index 1f6a77f2c..87e14f4df 100644 --- a/ethcore/src/trace/mod.rs +++ b/ethcore/src/trace/mod.rs @@ -96,6 +96,7 @@ pub trait VMTracer: Send { /// Consumes self and returns the VM trace. fn drain(self) -> Option; + } /// `DbExtras` provides an interface to query extra data which is not stored in tracesdb, diff --git a/ethcore/wasm/run/src/runner.rs b/ethcore/wasm/run/src/runner.rs index b2695fc8e..b04ec29a4 100644 --- a/ethcore/wasm/run/src/runner.rs +++ b/ethcore/wasm/run/src/runner.rs @@ -66,7 +66,7 @@ impl Fail { } impl fmt::Display for Fail { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::Fail::*; match *self { Return { ref expected, ref actual } => diff --git a/evmbin/Cargo.toml b/evmbin/Cargo.toml index 03c6e492a..b75890f61 100644 --- a/evmbin/Cargo.toml +++ b/evmbin/Cargo.toml @@ -11,7 +11,7 @@ path = "./src/main.rs" [dependencies] docopt = "1.0" env_logger = "0.5" -ethcore = { path = "../ethcore", features = ["test-helpers", "json-tests"] } +ethcore = { path = "../ethcore", features = ["test-helpers", "json-tests", "to-pod-full"] } ethjson = { path = "../json" } parity-bytes = "0.1" ethcore-transaction = { path = "../ethcore/transaction" } diff --git a/evmbin/README.md b/evmbin/README.md index 585be4228..49e761761 100644 --- a/evmbin/README.md +++ b/evmbin/README.md @@ -9,7 +9,7 @@ EVM implementation for Parity. Copyright 2015-2018 Parity Technologies (UK) Ltd. Usage: - parity-evm state-test [--json --std-json --only NAME --chain CHAIN] + parity-evm state-test [--json --std-json --std-dump-json --only NAME --chain CHAIN --std-out-only --std-err-only] parity-evm stats [options] parity-evm stats-jsontests-vm parity-evm [options] @@ -36,6 +36,11 @@ State test options: General options: --json Display verbose results in JSON. --std-json Display results in standardized JSON format. + --std-err-only With --std-json redirect to err output only. + --std-out-only With --std-json redirect to out output only. + --std-dump-json Display results in standardized JSON format + with additional state dump. +Display result state dump in standardized JSON format. --chain CHAIN Chain spec file path. -h, --help Display this message and exit. ``` diff --git a/evmbin/src/display/json.rs b/evmbin/src/display/json.rs index 9b1b7b10e..09d0a78f8 100644 --- a/evmbin/src/display/json.rs +++ b/evmbin/src/display/json.rs @@ -74,6 +74,8 @@ impl Informant { } impl vm::Informant for Informant { + type Sink = (); + fn before_test(&mut self, name: &str, action: &str) { println!("{}", json!({"action": action, "test": name})); } @@ -82,7 +84,9 @@ impl vm::Informant for Informant { self.gas_used = gas; } - fn finish(result: vm::RunResult) { + fn clone_sink(&self) -> Self::Sink { () } + + fn finish(result: vm::RunResult, _sink: &mut Self::Sink) { match result { Ok(success) => { for trace in success.traces.unwrap_or_else(Vec::new) { diff --git a/evmbin/src/display/simple.rs b/evmbin/src/display/simple.rs index 5cb53d4c2..9bfe86f07 100644 --- a/evmbin/src/display/simple.rs +++ b/evmbin/src/display/simple.rs @@ -27,11 +27,16 @@ use info as vm; pub struct Informant; impl vm::Informant for Informant { + + type Sink = (); + fn before_test(&mut self, name: &str, action: &str) { println!("Test: {} ({})", name, action); } - fn finish(result: vm::RunResult) { + fn clone_sink(&self) -> Self::Sink { () } + + fn finish(result: vm::RunResult, _sink: &mut Self::Sink) { match result { Ok(success) => { println!("Output: 0x{}", success.output.to_hex()); diff --git a/evmbin/src/display/std_json.rs b/evmbin/src/display/std_json.rs index 43b7fede1..87f515e8e 100644 --- a/evmbin/src/display/std_json.rs +++ b/evmbin/src/display/std_json.rs @@ -21,7 +21,7 @@ use std::io; use ethereum_types::{H256, U256}; use bytes::ToPretty; -use ethcore::trace; +use ethcore::{trace, pod_state}; use display; use info as vm; @@ -52,7 +52,7 @@ impl Writer for io::Stderr { } /// JSON formatting informant. -pub struct Informant { +pub struct Informant { code: Vec, instruction: u8, depth: usize, @@ -64,13 +64,28 @@ pub struct Informant { out_sink: Out, } -impl Default for Informant { +impl Default for Informant { fn default() -> Self { Self::new(io::stderr(), io::stdout()) } } +impl Informant { + /// std json informant using out only. + pub fn out_only() -> Self { + Self::new(io::stdout(), io::stdout()) + } +} + +impl Informant { + /// std json informant using err only. + pub fn err_only() -> Self { + Self::new(io::stderr(), io::stderr()) + } +} + impl Informant { + pub fn new(trace_sink: Trace, out_sink: Out) -> Self { Informant { code: Default::default(), @@ -91,9 +106,24 @@ impl Informant { Self::with_informant_in_depth(informant.subinfos.last_mut().expect("prepare/done_trace are not balanced"), depth - 1, f); } } + + fn dump_state_into(trace_sink: &mut Trace, root: H256, end_state: &Option) { + if let Some(ref end_state) = end_state { + let dump_data = json!({ + "root": root, + "accounts": end_state, + }); + writeln!(trace_sink, "{}", dump_data).expect("The sink must be writeable."); + } + } + + } impl vm::Informant for Informant { + + type Sink = (Trace, Out); + fn before_test(&mut self, name: &str, action: &str) { let out_data = json!({ "action": action, @@ -105,23 +135,26 @@ impl vm::Informant for Informant { fn set_gas(&mut self, _gas: U256) {} - fn finish(result: vm::RunResult<::Output>) { - let mut trace_sink = Trace::default(); - let mut out_sink = Out::default(); + fn clone_sink(&self) -> Self::Sink { + (self.trace_sink.clone(), self.out_sink.clone()) + } + fn finish(result: vm::RunResult<::Output>, (ref mut trace_sink, ref mut out_sink): &mut Self::Sink) { match result { Ok(success) => { let trace_data = json!({"stateRoot": success.state_root}); - writeln!(&mut trace_sink, "{}", trace_data) + writeln!(trace_sink, "{}", trace_data) .expect("The sink must be writeable."); + Self::dump_state_into(trace_sink, success.state_root, &success.end_state); + let out_data = json!({ "output": format!("0x{}", success.output.to_hex()), "gasUsed": format!("{:#x}", success.gas_used), "time": display::as_micros(&success.time), }); - writeln!(&mut out_sink, "{}", out_data).expect("The sink must be writeable."); + writeln!(out_sink, "{}", out_data).expect("The sink must be writeable."); }, Err(failure) => { let out_data = json!({ @@ -130,7 +163,9 @@ impl vm::Informant for Informant { "time": display::as_micros(&failure.time), }); - writeln!(&mut out_sink, "{}", out_data).expect("The sink must be writeable."); + Self::dump_state_into(trace_sink, failure.state_root, &failure.end_state); + + writeln!(out_sink, "{}", out_data).expect("The sink must be writeable."); }, } } @@ -200,6 +235,7 @@ impl trace::VMTracer for Informant { } fn drain(self) -> Option { None } + } #[cfg(test)] diff --git a/evmbin/src/info.rs b/evmbin/src/info.rs index dfb97e760..67df1d284 100644 --- a/evmbin/src/info.rs +++ b/evmbin/src/info.rs @@ -19,19 +19,23 @@ use std::time::{Instant, Duration}; use ethereum_types::{H256, U256}; use ethcore::client::{self, EvmTestClient, EvmTestError, TransactResult}; -use ethcore::{trace, spec, pod_state}; +use ethcore::{state, state_db, trace, spec, pod_state, TrieSpec}; use ethjson; use transaction; use vm::ActionParams; /// VM execution informant pub trait Informant: trace::VMTracer { + /// Sink to use with finish + type Sink; /// Display a single run init message fn before_test(&mut self, test: &str, action: &str); /// Set initial gas. fn set_gas(&mut self, _gas: U256) {} + /// Clone sink. + fn clone_sink(&self) -> Self::Sink; /// Display final result. - fn finish(result: RunResult); + fn finish(result: RunResult, &mut Self::Sink); } /// Execution finished correctly @@ -47,11 +51,15 @@ pub struct Success { pub time: Duration, /// Traces pub traces: Option, + /// Optional end state dump + pub end_state: Option, } /// Execution failed #[derive(Debug)] pub struct Failure { + /// State root + pub state_root: H256, /// Used gas pub gas_used: U256, /// Internal error @@ -60,6 +68,8 @@ pub struct Failure { pub time: Duration, /// Traces pub traces: Option, + /// Optional end state dump + pub end_state: Option, } /// EVM Execution result @@ -70,6 +80,7 @@ pub fn run_action( spec: &spec::Spec, mut params: ActionParams, mut informant: T, + trie_spec: TrieSpec, ) -> RunResult { informant.set_gas(params.gas); @@ -80,12 +91,12 @@ pub fn run_action( params.code_hash = None; } } - run(spec, params.gas, spec.genesis_state(), |mut client| { + run(spec, trie_spec, params.gas, spec.genesis_state(), |mut client| { let result = match client.call(params, &mut trace::NoopTracer, &mut informant) { - Ok(r) => (Ok((0.into(), r.return_data.to_vec())), Some(r.gas_left)), + Ok(r) => (Ok(r.return_data.to_vec()), Some(r.gas_left)), Err(err) => (Err(err), None), }; - (result.0, result.1, informant.drain()) + (result.0, 0.into(), None, result.1, informant.drain()) }) } @@ -99,6 +110,7 @@ pub fn run_transaction( env_info: &client::EnvInfo, transaction: transaction::SignedTransaction, mut informant: T, + trie_spec: TrieSpec, ) { let spec_name = format!("{:?}", spec).to_lowercase(); let spec = match EvmTestClient::spec_from_json(spec) { @@ -114,64 +126,82 @@ pub fn run_transaction( informant.set_gas(env_info.gas_limit); - let result = run(&spec, transaction.gas, pre_state, |mut client| { + let mut sink = informant.clone_sink(); + let result = run(&spec, trie_spec, transaction.gas, pre_state, |mut client| { let result = client.transact(env_info, transaction, trace::NoopTracer, informant); match result { - TransactResult::Ok { state_root, gas_left, .. } if state_root != post_root => { - (Err(EvmTestError::PostCondition(format!( - "State root mismatch (got: 0x{:x}, expected: 0x{:x})", - state_root, - post_root, - ))), Some(gas_left), None) + TransactResult::Ok { state_root, gas_left, output, vm_trace, end_state, .. } => { + if state_root != post_root { + (Err(EvmTestError::PostCondition(format!( + "State root mismatch (got: {:#x}, expected: {:#x})", + state_root, + post_root, + ))), state_root, end_state, Some(gas_left), None) + } else { + (Ok(output), state_root, end_state, Some(gas_left), vm_trace) + } }, - TransactResult::Ok { state_root, gas_left, output, vm_trace, .. } => { - (Ok((state_root, output)), Some(gas_left), vm_trace) - }, - TransactResult::Err { error, .. } => { + TransactResult::Err { state_root, error, end_state } => { (Err(EvmTestError::PostCondition(format!( "Unexpected execution error: {:?}", error - ))), None, None) + ))), state_root, end_state, None, None) }, } }); - T::finish(result) + T::finish(result, &mut sink) +} + +fn dump_state(state: &state::State) -> Option { + state.to_pod_full().ok() } /// Execute VM with given `ActionParams` pub fn run<'a, F, X>( spec: &'a spec::Spec, + trie_spec: TrieSpec, initial_gas: U256, pre_state: &'a pod_state::PodState, run: F, ) -> RunResult where - F: FnOnce(EvmTestClient) -> (Result<(H256, Vec), EvmTestError>, Option, Option), + F: FnOnce(EvmTestClient) -> (Result, EvmTestError>, H256, Option, Option, Option), { - let test_client = EvmTestClient::from_pod_state(spec, pre_state.clone()) + let do_dump = trie_spec == TrieSpec::Fat; + + let mut test_client = EvmTestClient::from_pod_state_with_trie(spec, pre_state.clone(), trie_spec) .map_err(|error| Failure { gas_used: 0.into(), error, time: Duration::from_secs(0), traces: None, + state_root: H256::default(), + end_state: None, })?; + if do_dump { + test_client.set_dump_state_fn(dump_state); + } + let start = Instant::now(); let result = run(test_client); let time = start.elapsed(); match result { - (Ok((state_root, output)), gas_left, traces) => Ok(Success { + (Ok(output), state_root, end_state, gas_left, traces) => Ok(Success { state_root, gas_used: gas_left.map(|gas_left| initial_gas - gas_left).unwrap_or(initial_gas), output, time, traces, + end_state, }), - (Err(error), gas_left, traces) => Err(Failure { + (Err(error), state_root, end_state, gas_left, traces) => Err(Failure { gas_used: gas_left.map(|gas_left| initial_gas - gas_left).unwrap_or(initial_gas), error, time, traces, + state_root, + end_state, }), } } @@ -200,7 +230,7 @@ pub mod tests { let tempdir = TempDir::new("").unwrap(); let spec = ::ethcore::ethereum::new_foundation(&tempdir.path()); - let result = run_action(&spec, params, informant); + let result = run_action(&spec, params, informant, TrieSpec::Secure); match result { Ok(Success { traces, .. }) => { compare(traces, expected) @@ -221,7 +251,7 @@ pub mod tests { params.gas = 0xffff.into(); let spec = ::ethcore::ethereum::load(None, include_bytes!("../res/testchain.json")); - let _result = run_action(&spec, params, inf); + let _result = run_action(&spec, params, inf, TrieSpec::Secure); assert_eq!( &String::from_utf8_lossy(&**res.lock().unwrap()), diff --git a/evmbin/src/main.rs b/evmbin/src/main.rs index ded5c321c..41bfbe1bd 100644 --- a/evmbin/src/main.rs +++ b/evmbin/src/main.rs @@ -49,7 +49,7 @@ use docopt::Docopt; use rustc_hex::FromHex; use ethereum_types::{U256, Address}; use bytes::Bytes; -use ethcore::{spec, json_tests}; +use ethcore::{spec, json_tests, TrieSpec}; use vm::{ActionParams, CallType}; mod info; @@ -62,7 +62,7 @@ EVM implementation for Parity. Copyright 2015-2018 Parity Technologies (UK) Ltd. Usage: - parity-evm state-test [--json --std-json --only NAME --chain CHAIN] + parity-evm state-test [--json --std-json --std-dump-json --only NAME --chain CHAIN --std-out-only --std-err-only] parity-evm stats [options] parity-evm stats-jsontests-vm parity-evm [options] @@ -89,6 +89,11 @@ State test options: General options: --json Display verbose results in JSON. --std-json Display results in standardized JSON format. + --std-err-only With --std-json redirect to err output only. + --std-out-only With --std-json redirect to out output only. + --std-dump-json Display results in standardized JSON format + with additional state dump. +Display result state dump in standardized JSON format. --chain CHAIN Chain spec file path. -h, --help Display this message and exit. "#; @@ -105,8 +110,14 @@ fn main() { run_stats_jsontests_vm(args) } else if args.flag_json { run_call(args, display::json::Informant::default()) - } else if args.flag_std_json { - run_call(args, display::std_json::Informant::default()) + } else if args.flag_std_dump_json || args.flag_std_json { + if args.flag_std_err_only { + run_call(args, display::std_json::Informant::err_only()) + } else if args.flag_std_out_only { + run_call(args, display::std_json::Informant::out_only()) + } else { + run_call(args, display::std_json::Informant::default()) + }; } else { run_call(args, display::simple::Informant::default()) } @@ -179,15 +190,23 @@ fn run_state_test(args: Args) { 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 if args.flag_std_json { - let i = display::std_json::Informant::default(); - info::run_transaction(&name, idx, &spec, &pre, post_root, &env_info, transaction, i) + let trie_spec = if args.flag_std_dump_json { + TrieSpec::Fat } else { - let i = display::simple::Informant::default(); - info::run_transaction(&name, idx, &spec, &pre, post_root, &env_info, transaction, i) + TrieSpec::Secure + }; + if args.flag_json { + info::run_transaction(&name, idx, &spec, &pre, post_root, &env_info, transaction, display::json::Informant::default(), trie_spec) + } else if args.flag_std_dump_json || args.flag_std_json { + if args.flag_std_err_only { + info::run_transaction(&name, idx, &spec, &pre, post_root, &env_info, transaction, display::std_json::Informant::err_only(), trie_spec) + } else if args.flag_std_out_only { + info::run_transaction(&name, idx, &spec, &pre, post_root, &env_info, transaction, display::std_json::Informant::out_only(), trie_spec) + } else { + info::run_transaction(&name, idx, &spec, &pre, post_root, &env_info, transaction, display::std_json::Informant::default(), trie_spec) + } + } else { + info::run_transaction(&name, idx, &spec, &pre, post_root, &env_info, transaction, display::simple::Informant::default(), trie_spec) } } } @@ -218,8 +237,13 @@ fn run_call(args: Args, informant: T) { params.code = code.map(Arc::new); params.data = data; - let result = info::run_action(&spec, params, informant); - T::finish(result); + let mut sink = informant.clone_sink(); + let result = if args.flag_std_dump_json { + info::run_action(&spec, params, informant, TrieSpec::Fat) + } else { + info::run_action(&spec, params, informant, TrieSpec::Secure) + }; + T::finish(result, &mut sink); } #[derive(Debug, Deserialize)] @@ -238,6 +262,9 @@ struct Args { flag_chain: Option, flag_json: bool, flag_std_json: bool, + flag_std_dump_json: bool, + flag_std_err_only: bool, + flag_std_out_only: bool, } impl Args { @@ -285,7 +312,7 @@ impl Args { pub fn spec(&self) -> Result { Ok(match self.flag_chain { - Some(ref filename) => { + Some(ref filename) => { let file = fs::File::open(filename).map_err(|e| format!("{}", e))?; spec::Spec::load(&::std::env::temp_dir(), file)? }, @@ -324,17 +351,21 @@ mod tests { "parity-evm", "--json", "--std-json", + "--std-dump-json", "--gas", "1", "--gas-price", "2", "--from", "0000000000000000000000000000000000000003", "--to", "0000000000000000000000000000000000000004", "--code", "05", "--input", "06", - "--chain", "./testfile", + "--chain", "./testfile", "--std-err-only", "--std-out-only" ]); assert_eq!(args.flag_json, true); assert_eq!(args.flag_std_json, true); + assert_eq!(args.flag_std_dump_json, true); + assert_eq!(args.flag_std_err_only, true); + assert_eq!(args.flag_std_out_only, true); assert_eq!(args.gas(), Ok(1.into())); assert_eq!(args.gas_price(), Ok(2.into())); assert_eq!(args.from(), Ok(3.into())); @@ -353,13 +384,15 @@ mod tests { "--chain", "homestead", "--only=add11", "--json", - "--std-json" + "--std-json", + "--std-dump-json" ]); assert_eq!(args.cmd_state_test, true); assert!(args.arg_file.is_some()); assert_eq!(args.flag_json, true); assert_eq!(args.flag_std_json, true); + assert_eq!(args.flag_std_dump_json, true); assert_eq!(args.flag_chain, Some("homestead".to_owned())); assert_eq!(args.flag_only, Some("add11".to_owned())); }