From 92f08bea7e70975b019cd2fb2336b00d701f5251 Mon Sep 17 00:00:00 2001 From: pistomat Date: Thu, 16 Feb 2023 12:53:12 +0100 Subject: [PATCH] feat: json opcode traces EIP-3155 (#356) * EIP 3155 tracing Partial implementation of 3155 tracing. * fix lint issues * restructure tracer eip3155 * Update crates/revm/Cargo.toml Co-authored-by: rakita * Update crates/revm/src/inspector.rs Co-authored-by: rakita * default to single threaded run for traces * Update Cargo.toml * Update crates/revm/src/inspector/tracer_eip3155.rs * fmt --------- Co-authored-by: Danno Ferrin --- .gitignore | 1 + Cargo.lock | 2 + bins/revme/Cargo.toml | 2 +- bins/revme/src/statetest/cmd.rs | 4 +- bins/revme/src/statetest/runner.rs | 58 +++++- crates/revm/Cargo.toml | 3 +- crates/revm/src/inspector.rs | 4 + crates/revm/src/inspector/gas.rs | 12 ++ crates/revm/src/inspector/tracer_eip3155.rs | 187 ++++++++++++++++++++ 9 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 crates/revm/src/inspector/tracer_eip3155.rs diff --git a/.gitignore b/.gitignore index 1a2e291d3b..3831db64e9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ target .vscode +.idea pkg/ diff --git a/Cargo.lock b/Cargo.lock index d4031dcb94..13d11c9173 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1587,6 +1587,7 @@ dependencies = [ "revm-interpreter", "revm-precompile", "serde", + "serde_json", "tokio", ] @@ -1934,6 +1935,7 @@ version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" dependencies = [ + "indexmap", "itoa", "ryu", "serde", diff --git a/bins/revme/Cargo.toml b/bins/revme/Cargo.toml index 46512728c5..27a09158fa 100644 --- a/bins/revme/Cargo.toml +++ b/bins/revme/Cargo.toml @@ -26,7 +26,7 @@ revm = { path = "../../crates/revm", version = "3.0.0", default-features = false rlp = { version = "0.5", default-features = false } ruint = { version = "1.7.0", features = ["rlp", "serde"] } serde = { version = "1.0", features = ["derive", "rc"] } -serde_json = "1.0" +serde_json = { version = "1.0", features = ["preserve_order"] } sha3 = { version = "0.10", default-features = false } structopt = "0.3" thiserror = "1.0" diff --git a/bins/revme/src/statetest/cmd.rs b/bins/revme/src/statetest/cmd.rs index 4ca4b588ae..f66aa6d706 100644 --- a/bins/revme/src/statetest/cmd.rs +++ b/bins/revme/src/statetest/cmd.rs @@ -9,6 +9,8 @@ pub struct Cmd { path: Vec, #[structopt(short = "s", long)] single_thread: bool, + #[structopt(long)] + json: bool, } impl Cmd { @@ -16,7 +18,7 @@ impl Cmd { for path in &self.path { println!("Start running tests on: {path:?}"); let test_files = find_all_json_tests(path); - run(test_files, self.single_thread)? + run(test_files, self.single_thread, self.json)? } Ok(()) } diff --git a/bins/revme/src/statetest/runner.rs b/bins/revme/src/statetest/runner.rs index aafb1434b3..8f802d287c 100644 --- a/bins/revme/src/statetest/runner.rs +++ b/bins/revme/src/statetest/runner.rs @@ -1,3 +1,4 @@ +use std::io::stdout; use std::{ collections::HashMap, ffi::OsStr, @@ -7,9 +8,10 @@ use std::{ }; use indicatif::ProgressBar; + +use revm::inspectors::TracerEip3155; use revm::{ db::AccountState, - inspectors::CustomPrintTracer, interpreter::CreateScheme, primitives::{Bytecode, Env, ExecutionResult, SpecId, TransactTo, B160, B256, U256}, }; @@ -50,7 +52,11 @@ pub fn find_all_json_tests(path: &Path) -> Vec { .collect::>() } -pub fn execute_test_suit(path: &Path, elapsed: &Arc>) -> Result<(), TestError> { +pub fn execute_test_suit( + path: &Path, + elapsed: &Arc>, + trace: bool, +) -> Result<(), TestError> { // funky test with `bigint 0x00` value in json :) not possible to happen on mainnet and require custom json parser. // https://github.com/ethereum/tests/issues/971 if path.file_name() == Some(OsStr::new("ValueOverflow.json")) { @@ -248,7 +254,12 @@ pub fn execute_test_suit(path: &Path, elapsed: &Arc>) -> Result< // do the deed let timer = Instant::now(); - let out = evm.transact_commit(); + + let exec_result = if trace { + evm.inspect_commit(TracerEip3155::new(Box::new(stdout()), false, false)) + } else { + evm.transact_commit() + }; let timer = timer.elapsed(); *elapsed.lock().unwrap() += timer; @@ -269,7 +280,7 @@ pub fn execute_test_suit(path: &Path, elapsed: &Arc>) -> Result< }) .map(|(k, v)| (*k, v.clone())), ); - let logs = match &out { + let logs = match &exec_result { Ok(ExecutionResult::Success { logs, .. }) => logs.clone(), _ => Vec::new(), }; @@ -281,10 +292,33 @@ pub fn execute_test_suit(path: &Path, elapsed: &Arc>) -> Result< ); let mut database_cloned = database.clone(); evm.database(&mut database_cloned); - let _ = evm.inspect_commit(CustomPrintTracer::default()); + let _ = + evm.inspect_commit(TracerEip3155::new(Box::new(stdout()), false, false)); let db = evm.db().unwrap(); println!("{path:?} UNIT_TEST:{name}\n"); - println!("Output: {out:?}"); + match &exec_result { + Ok(ExecutionResult::Success { + reason, + gas_used, + gas_refunded, + .. + }) => { + println!("Failed reason: {reason:?} {path:?} UNIT_TEST:{name}\n gas:{gas_used:?} ({gas_refunded:?} refunded)"); + } + Ok(ExecutionResult::Revert { gas_used, output }) => { + println!( + "Reverted: {output:?} {path:?} UNIT_TEST:{name}\n gas:{gas_used:?}" + ); + } + Ok(ExecutionResult::Halt { reason, gas_used }) => { + println!( + "Halted: {reason:?} {path:?} UNIT_TEST:{name}\n gas:{gas_used:?}" + ); + } + Err(out) => { + println!("Output: {out:?} {path:?} UNIT_TEST:{name}\n"); + } + } println!("\nApplied state:{db:?}\n"); println!("\nStateroot: {state_root:?}\n"); return Err(TestError::RootMissmatch { @@ -300,7 +334,15 @@ pub fn execute_test_suit(path: &Path, elapsed: &Arc>) -> Result< Ok(()) } -pub fn run(test_files: Vec, single_thread: bool) -> Result<(), TestError> { +pub fn run( + test_files: Vec, + mut single_thread: bool, + trace: bool, +) -> Result<(), TestError> { + if trace { + single_thread = true; + } + let endjob = Arc::new(AtomicBool::new(false)); let console_bar = Arc::new(ProgressBar::new(test_files.len() as u64)); let mut joins: Vec>> = Vec::new(); @@ -330,7 +372,7 @@ pub fn run(test_files: Vec, single_thread: bool) -> Result<(), TestErro return Ok(()); } //println!("Test:{:?}\n",test_path); - if let Err(err) = execute_test_suit(&test_path, &elapsed) { + if let Err(err) = execute_test_suit(&test_path, &elapsed, trace) { endjob.store(true, Ordering::SeqCst); println!("Test[{index}] named:\n{test_path:?} failed: {err}\n"); return Err(err); diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index f86655b658..ffb841e7ef 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -17,6 +17,7 @@ auto_impl = { version = "1.0", default-features = false } # Optional serde = { version = "1.0", features = ["derive", "rc"], optional = true } +serde_json = { version = "1.0", features = ["preserve_order"], optional = true } # ethersdb tokio = { version = "1.25", features = [ @@ -49,7 +50,7 @@ optional_eip3607 = ["revm-interpreter/optional_eip3607"] optional_gas_refund = ["revm-interpreter/optional_gas_refund"] std = ["revm-interpreter/std"] ethersdb = ["tokio", "futures", "ethers-providers", "ethers-core"] -serde = ["dep:serde", "revm-interpreter/serde"] +serde = ["dep:serde","dep:serde_json", "revm-interpreter/serde"] # deprecated feature web3db = [] with-serde = [] diff --git a/crates/revm/src/inspector.rs b/crates/revm/src/inspector.rs index 32e43c6983..7ab77be886 100644 --- a/crates/revm/src/inspector.rs +++ b/crates/revm/src/inspector.rs @@ -8,6 +8,8 @@ use auto_impl::auto_impl; pub mod customprinter; pub mod gas; pub mod noop; +#[cfg(feature = "std")] +pub mod tracer_eip3155; /// All Inspectors implementations that revm has. pub mod inspectors { @@ -15,6 +17,8 @@ pub mod inspectors { pub use super::customprinter::CustomPrintTracer; pub use super::gas::GasInspector; pub use super::noop::NoOpInspector; + #[cfg(feature = "std")] + pub use super::tracer_eip3155::TracerEip3155; } #[auto_impl(&mut, Box)] diff --git a/crates/revm/src/inspector/gas.rs b/crates/revm/src/inspector/gas.rs index a92b9bdd19..fa739aef78 100644 --- a/crates/revm/src/inspector/gas.rs +++ b/crates/revm/src/inspector/gas.rs @@ -15,12 +15,17 @@ pub struct GasInspector { was_jumpi: Option, gas_remaining: u64, + last_gas_cost: u64, } impl GasInspector { pub fn gas_remaining(&self) -> u64 { self.gas_remaining } + + pub fn last_gas_cost(&self) -> u64 { + self.last_gas_cost + } } impl Inspector for GasInspector { @@ -90,8 +95,15 @@ impl Inspector for GasInspector { self.full_gas_block = interp.contract.gas_block(previous_pc); self.was_return = false; } + + let last_gas = self.gas_remaining; self.gas_remaining = interp.gas.remaining() + (self.full_gas_block - self.reduced_gas_block); + if last_gas > self.gas_remaining { + self.last_gas_cost = last_gas - self.gas_remaining; + } else { + self.last_gas_cost = 0; + } InstructionResult::Continue } diff --git a/crates/revm/src/inspector/tracer_eip3155.rs b/crates/revm/src/inspector/tracer_eip3155.rs new file mode 100644 index 0000000000..a9b80c9a53 --- /dev/null +++ b/crates/revm/src/inspector/tracer_eip3155.rs @@ -0,0 +1,187 @@ +//! Inspector that support tracing of EIP-3155 https://eips.ethereum.org/EIPS/eip-3155 + +use crate::inspectors::GasInspector; +use crate::interpreter::{CallInputs, CreateInputs, Gas, InstructionResult}; +use crate::primitives::{db::Database, hex, Bytes, B160}; +use crate::{evm_impl::EVMData, Inspector}; +use revm_interpreter::primitives::U256; +use revm_interpreter::{opcode, Interpreter, Memory, Stack}; +use serde_json::json; +use std::io::Write; + +pub struct TracerEip3155 { + output: Box, + gas_inspector: GasInspector, + + #[allow(dead_code)] + trace_mem: bool, + #[allow(dead_code)] + trace_return_data: bool, + + stack: Stack, + pc: usize, + opcode: u8, + gas: u64, + mem_size: usize, + #[allow(dead_code)] + memory: Option, + skip: bool, +} + +impl TracerEip3155 { + pub fn new(output: Box, trace_mem: bool, trace_return_data: bool) -> Self { + Self { + output, + gas_inspector: GasInspector::default(), + trace_mem, + trace_return_data, + stack: Stack::new(), + pc: 0, + opcode: 0, + gas: 0, + mem_size: 0, + memory: None, + skip: false, + } + } +} + +impl Inspector for TracerEip3155 { + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + data: &mut EVMData<'_, DB>, + is_static: bool, + ) -> InstructionResult { + self.gas_inspector + .initialize_interp(interp, data, is_static); + InstructionResult::Continue + } + + // get opcode by calling `interp.contract.opcode(interp.program_counter())`. + // all other information can be obtained from interp. + fn step( + &mut self, + interp: &mut Interpreter, + data: &mut EVMData<'_, DB>, + is_static: bool, + ) -> InstructionResult { + self.gas_inspector.step(interp, data, is_static); + self.stack = interp.stack.clone(); + self.pc = interp.program_counter(); + self.opcode = interp.current_opcode(); + self.mem_size = interp.memory.len(); + self.gas = self.gas_inspector.gas_remaining(); + // + InstructionResult::Continue + } + + fn step_end( + &mut self, + interp: &mut Interpreter, + data: &mut EVMData<'_, DB>, + is_static: bool, + eval: InstructionResult, + ) -> InstructionResult { + self.gas_inspector.step_end(interp, data, is_static, eval); + if self.skip { + self.skip = false; + return InstructionResult::Continue; + }; + + self.print_log_line(data.journaled_state.depth()); + InstructionResult::Continue + } + + fn call( + &mut self, + data: &mut EVMData<'_, DB>, + _inputs: &mut CallInputs, + _is_static: bool, + ) -> (InstructionResult, Gas, Bytes) { + self.print_log_line(data.journaled_state.depth()); + (InstructionResult::Continue, Gas::new(0), Bytes::new()) + } + + fn call_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CallInputs, + remaining_gas: Gas, + ret: InstructionResult, + out: Bytes, + is_static: bool, + ) -> (InstructionResult, Gas, Bytes) { + self.gas_inspector + .call_end(data, inputs, remaining_gas, ret, out.clone(), is_static); + // self.log_step(interp, data, is_static, eval); + self.skip = true; + if data.journaled_state.depth() == 0 { + let log_line = json!({ + //stateroot + "output": format!("{out:?}"), + "gasUser": format!("0x{:x}", self.gas_inspector.gas_remaining()), + //time + //fork + }); + + writeln!( + self.output, + "{:?}", + serde_json::to_string(&log_line).unwrap() + ) + .expect("If output fails we can ignore the logging"); + } + (ret, remaining_gas, out) + } + + fn create_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CreateInputs, + ret: InstructionResult, + address: Option, + remaining_gas: Gas, + out: Bytes, + ) -> (InstructionResult, Option, Gas, Bytes) { + self.gas_inspector + .create_end(data, inputs, ret, address, remaining_gas, out.clone()); + (ret, address, remaining_gas, out) + } +} + +impl TracerEip3155 { + fn print_log_line(&mut self, depth: u64) { + let short_stack: Vec = self.stack.data().iter().map(|&b| short_hex(b)).collect(); + let log_line = json!({ + "pc": self.pc, + "op": self.opcode, + "gas": format!("0x{:x}", self.gas), + "gasCost": format!("0x{:x}", self.gas_inspector.last_gas_cost()), + //memory? + "memSize": self.mem_size, + "stack": short_stack, + "depth": depth, + //returnData + //refund + "opName": opcode::OPCODE_JUMPMAP[self.opcode as usize], + //error + //storage + //returnStack + }); + + writeln!(self.output, "{}", serde_json::to_string(&log_line).unwrap()) + .expect("If output fails we can ignore the logging"); + } +} + +fn short_hex(b: U256) -> String { + let s = hex::encode(b.to_be_bytes_vec()) + .trim_start_matches('0') + .to_string(); + if s.is_empty() { + "0x0".to_string() + } else { + format!("0x{s}") + } +}