diff --git a/Cargo.lock b/Cargo.lock index 408eae0e1..b5771c846 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,8 +196,11 @@ dependencies = [ "byte-slice-cast", "criterion", "engine-standalone-storage", + "engine-standalone-tracing", "ethabi", "evm", + "evm-gasometer", + "evm-runtime", "git2", "hex", "libsecp256k1", @@ -1196,6 +1199,7 @@ dependencies = [ "aurora-engine-types", "borsh 0.8.2", "engine-standalone-storage", + "engine-standalone-tracing", "evm-core", "libc", "rocksdb", @@ -1213,6 +1217,19 @@ dependencies = [ "rocksdb", ] +[[package]] +name = "engine-standalone-tracing" +version = "0.1.0" +dependencies = [ + "aurora-engine", + "aurora-engine-sdk", + "aurora-engine-types", + "evm", + "evm-core", + "evm-gasometer", + "evm-runtime", +] + [[package]] name = "enumset" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index ef80ad1ee..9095e16bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "engine-sdk", "engine-standalone", "engine-standalone-storage", + "engine-standalone-tracing", "engine-tests", "engine-types", ] diff --git a/engine-standalone-tracing/Cargo.toml b/engine-standalone-tracing/Cargo.toml new file mode 100644 index 000000000..e2b304d19 --- /dev/null +++ b/engine-standalone-tracing/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "engine-standalone-tracing" +version = "0.1.0" +edition = "2018" +authors = ["Aurora "] +description = "Aurora engine standalone tracing library. Provides functions and types for extracing geth-like traces from standalone engine execution." +homepage = "https://github.com/aurora-is-near/aurora-engine" +repository = "https://github.com/aurora-is-near/aurora-engine" +license = "CC0-1.0" +publish = false +autobenches = false + +[lib] +crate-type = ["lib"] + +[dependencies] +aurora-engine = { path = "../engine", default-features = false, features = ["std"] } +aurora-engine-types = { path = "../engine-types", default-features = false, features = ["std"] } +aurora-engine-sdk = { path = "../engine-sdk", default-features = false, features = ["std"] } +evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false, features = ["std"] } +evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false, features = ["std", "tracing"] } +evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false, features = ["std", "tracing"] } +evm-gasometer = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false, features = ["std", "tracing"] } + +[features] +default = [] +mainnet = [] +testnet = [] +betanet = [] diff --git a/engine-standalone-tracing/src/lib.rs b/engine-standalone-tracing/src/lib.rs new file mode 100644 index 000000000..da3f130ae --- /dev/null +++ b/engine-standalone-tracing/src/lib.rs @@ -0,0 +1,7 @@ +pub mod sputnik; +pub mod types; + +pub use types::{ + Depth, LogMemory, LogStack, LogStorage, LogStorageKey, LogStorageValue, Logs, ProgramCounter, + StepTransactionTrace, TraceLog, TransactionTrace, +}; diff --git a/engine-standalone-tracing/src/sputnik.rs b/engine-standalone-tracing/src/sputnik.rs new file mode 100644 index 000000000..cc229014b --- /dev/null +++ b/engine-standalone-tracing/src/sputnik.rs @@ -0,0 +1,79 @@ +use std::cell::RefCell; +use std::ptr::NonNull; +use std::rc::Rc; + +// TODO: implement evm* tracing traits for `crate::trace::Logs`, and tie this `traced_call` function to the FFI. +#[allow(dead_code)] +/// Capture all events from SputnikVM emitted from within the given closure using the given listener. +pub fn traced_call(listener: &mut T, f: F) -> R +where + T: evm_gasometer::tracing::EventListener + + evm_runtime::tracing::EventListener + + evm::tracing::EventListener + + 'static, + F: FnOnce() -> R, +{ + let mut gas_listener = SharedMutableReference::new(listener); + let mut runtime_listener = gas_listener.clone(); + let mut evm_listener = gas_listener.clone(); + + evm_gasometer::tracing::using(&mut gas_listener, || { + evm_runtime::tracing::using(&mut runtime_listener, || { + evm::tracing::using(&mut evm_listener, f) + }) + }) +} + +/// This structure is intentionally private to this module as it is memory unsafe (contains a raw pointer). +/// Its purpose here is to allow a single event handling object to be used as the listener for +/// all SputnikVM events. It is needed because the listener must be passed as an object with a `'static` +/// lifetime, hence a normal reference cannot be used and we resort to raw pointers. The usage of this +/// struct in this module is safe because the `SharedMutableReference` objects created do not outlive +/// the reference they are based on (see `pub fn traced_call`). Moreover, because the SputnikVM code +/// is single-threaded, we do not need to worry about race conditions. +struct SharedMutableReference { + pointer: Rc>>, +} + +impl SharedMutableReference { + fn new(reference: &mut T) -> Self { + let ptr = NonNull::new(reference as _).unwrap(); + Self { + pointer: Rc::new(RefCell::new(ptr)), + } + } + + fn clone(&self) -> Self { + Self { + pointer: Rc::clone(&self.pointer), + } + } +} + +impl evm_gasometer::tracing::EventListener + for SharedMutableReference +{ + fn event(&mut self, event: evm_gasometer::tracing::Event) { + unsafe { + self.pointer.borrow_mut().as_mut().event(event); + } + } +} + +impl evm_runtime::tracing::EventListener + for SharedMutableReference +{ + fn event(&mut self, event: evm_runtime::tracing::Event) { + unsafe { + self.pointer.borrow_mut().as_mut().event(event); + } + } +} + +impl evm::tracing::EventListener for SharedMutableReference { + fn event(&mut self, event: evm::tracing::Event) { + unsafe { + self.pointer.borrow_mut().as_mut().event(event); + } + } +} diff --git a/engine-standalone/src/trace.rs b/engine-standalone-tracing/src/types.rs similarity index 100% rename from engine-standalone/src/trace.rs rename to engine-standalone-tracing/src/types.rs diff --git a/engine-standalone/Cargo.toml b/engine-standalone/Cargo.toml index 79421f38a..065e9d128 100644 --- a/engine-standalone/Cargo.toml +++ b/engine-standalone/Cargo.toml @@ -11,12 +11,13 @@ publish = false autobenches = false [dependencies] -aurora-engine = { path = "../engine", default-features = false, features = ["std"] } +aurora-engine = { path = "../engine", default-features = false, features = ["std", "tracing"] } aurora-engine-types = { path = "../engine-types", default-features = false, features = ["std"] } aurora-engine-sdk = { path = "../engine-sdk", default-features = false, features = ["std"] } engine-standalone-storage = { path = "../engine-standalone-storage", default-features = false } +engine-standalone-tracing = { path = "../engine-standalone-tracing", default-features = false } borsh = { version = "0.8.2" } -evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false } +evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false, features = ["std"] } libc = "0.2" rocksdb = "0.16.0" diff --git a/engine-standalone/src/ffi.rs b/engine-standalone/src/ffi.rs index 5df4e4716..363a2237f 100644 --- a/engine-standalone/src/ffi.rs +++ b/engine-standalone/src/ffi.rs @@ -1,4 +1,4 @@ -use crate::trace::{TraceLog, TransactionTrace}; +use engine_standalone_tracing::{TraceLog, TransactionTrace}; use libc::{c_uchar, c_uint, c_ushort, size_t, uintmax_t}; use std::ffi::CString; diff --git a/engine-standalone/src/main.rs b/engine-standalone/src/main.rs index 28c66a5aa..f0f7c4ed6 100644 --- a/engine-standalone/src/main.rs +++ b/engine-standalone/src/main.rs @@ -1,5 +1,4 @@ mod ffi; -mod trace; fn main() { println!("Hello, World!"); diff --git a/engine-tests/Cargo.toml b/engine-tests/Cargo.toml index 6a20f73c7..4dc57ace3 100644 --- a/engine-tests/Cargo.toml +++ b/engine-tests/Cargo.toml @@ -13,15 +13,18 @@ publish = false autobenches = false [dependencies] -aurora-engine = { path = "../engine", default-features = false } -aurora-engine-types = { path = "../engine-types", default-features = false } -aurora-engine-sdk = { path = "../engine-sdk", default-features = false } +aurora-engine = { path = "../engine", default-features = false, features = ["std", "tracing"] } +aurora-engine-types = { path = "../engine-types", default-features = false, features = ["std"] } +aurora-engine-sdk = { path = "../engine-sdk", default-features = false, features = ["std"] } aurora-engine-precompiles = { path = "../engine-precompiles", default-features = false } engine-standalone-storage = { path = "../engine-standalone-storage", default-features = false } +engine-standalone-tracing = { path = "../engine-standalone-tracing", default-features = false } borsh = { version = "0.8.2", default-features = false } byte-slice-cast = { version = "1.0", default-features = false } sha3 = { version = "0.9.1", default-features = false } -evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false } +evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false, features = ["std", "tracing"] } +evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false, features = ["std", "tracing"] } +evm-gasometer = { git = "https://github.com/aurora-is-near/sputnikvm.git", default-features = false, features = ["std", "tracing"] } rlp = { version = "0.5.0", default-features = false } [dev-dependencies] diff --git a/engine-tests/src/test_utils/standalone/mocks/mod.rs b/engine-tests/src/test_utils/standalone/mocks/mod.rs index 56560a570..048ee814c 100644 --- a/engine-tests/src/test_utils/standalone/mocks/mod.rs +++ b/engine-tests/src/test_utils/standalone/mocks/mod.rs @@ -11,6 +11,7 @@ use crate::test_utils; pub mod block; pub mod promise; pub mod storage; +pub mod tracing; pub fn compute_block_hash(block_height: u64) -> H256 { aurora_engine::engine::compute_block_hash([0u8; 32], block_height, b"aurora") diff --git a/engine-tests/src/test_utils/standalone/mocks/tracing.rs b/engine-tests/src/test_utils/standalone/mocks/tracing.rs new file mode 100644 index 000000000..8dc0225c7 --- /dev/null +++ b/engine-tests/src/test_utils/standalone/mocks/tracing.rs @@ -0,0 +1,22 @@ +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Listener { + pub events: Vec, +} + +impl evm_gasometer::tracing::EventListener for Listener { + fn event(&mut self, event: evm_gasometer::tracing::Event) { + self.events.push(format!("{:?}", event)); + } +} + +impl evm_runtime::tracing::EventListener for Listener { + fn event(&mut self, event: evm_runtime::tracing::Event) { + self.events.push(format!("{:?}", event)); + } +} + +impl evm::tracing::EventListener for Listener { + fn event(&mut self, event: evm::tracing::Event) { + self.events.push(format!("{:?}", event)); + } +} diff --git a/engine-tests/src/tests/standalone/mod.rs b/engine-tests/src/tests/standalone/mod.rs index 2650ab986..d460953d4 100644 --- a/engine-tests/src/tests/standalone/mod.rs +++ b/engine-tests/src/tests/standalone/mod.rs @@ -1,2 +1,3 @@ mod sanity; mod storage; +mod tracing; diff --git a/engine-tests/src/tests/standalone/tracing.rs b/engine-tests/src/tests/standalone/tracing.rs new file mode 100644 index 000000000..9985580d9 --- /dev/null +++ b/engine-tests/src/tests/standalone/tracing.rs @@ -0,0 +1,128 @@ +use aurora_engine_types::{types::Wei, Address, U256}; +use engine_standalone_tracing::sputnik; + +use crate::test_utils::{self, standalone}; + +/// Test based on expected trace of +/// https://rinkeby.etherscan.io/tx/0xfc9359e49278b7ba99f59edac0e3de49956e46e530a53c15aa71226b7aa92c6f +/// (geth example found at https://gist.github.com/karalabe/c91f95ac57f5e57f8b950ec65ecc697f). +#[test] +fn test_evm_tracing() { + let mut runner = standalone::StandaloneRunner::default(); + let mut signer = test_utils::Signer::random(); + + // Initialize EVM + runner.init_evm(); + + // Deploy contract + let deploy_tx = aurora_engine::transaction::legacy::TransactionLegacy { + nonce: signer.use_nonce().into(), + gas_price: U256::zero(), + gas_limit: u64::MAX.into(), + to: None, + value: Wei::zero(), + data: hex::decode(CONTRACT_CODE).unwrap(), + }; + let result = runner + .submit_transaction(&signer.secret_key, deploy_tx) + .unwrap(); + let contract_address = Address::from_slice(test_utils::unwrap_success_slice(&result)); + + // Interact with contract (and trace execution) + let tx = aurora_engine::transaction::legacy::TransactionLegacy { + nonce: signer.use_nonce().into(), + gas_price: U256::zero(), + gas_limit: 90_000.into(), + to: Some(contract_address), + value: Wei::zero(), + data: hex::decode(CONTRACT_INPUT).unwrap(), + }; + let mut listener = standalone::mocks::tracing::Listener::default(); + let result = sputnik::traced_call(&mut listener, || { + runner.submit_transaction(&signer.secret_key, tx).unwrap() + }); + assert!(result.status.is_ok()); + + // Check trace + let positions: Vec = listener + .events + .iter() + .filter_map(|e| { + if e.starts_with("Step ") { + Some(parse_numer_in_parentheses("position:", e)) + } else { + None + } + }) + .collect(); + assert_eq!(positions.as_slice(), &EXPECTED_POSITIONS); + + let costs: Vec = listener + .events + .iter() + .filter_map(|e| { + if e.starts_with("RecordCost") { + Some(parse_field("cost:", e).parse().unwrap()) + } else if e.starts_with("RecordDynamicCost") { + let gas_cost: u32 = parse_field("gas_cost:", e).parse().unwrap(); + let memory_gas: u32 = parse_field("memory_gas:", e).parse().unwrap(); + Some(gas_cost + memory_gas) + } else { + None + } + }) + .skip(1) // The first cost is for the transaction itself; not included in geth trace + .collect(); + assert_eq!(costs.as_slice(), &EXPECTED_COSTS); + + let op_codes: Vec = listener + .events + .iter() + .filter_map(|e| { + if e.starts_with("Step ") { + Some(parse_numer_in_parentheses("opcode:", e)) + } else { + None + } + }) + .collect(); + assert_eq!(op_codes.as_slice(), &EXPECTED_OP_CODES); +} + +fn parse_field<'a>(name: &'static str, data: &'a str) -> &'a str { + data.split(name) + .skip(1) + .next() + .unwrap() + .trim() + .split(',') + .next() + .unwrap() +} + +fn parse_numer_in_parentheses(name: &'static str, data: &str) -> u8 { + let token = parse_field(name, data); + let number = token + .split('(') + .skip(1) + .next() + .unwrap() + .split(')') + .next() + .unwrap(); + number.parse().unwrap() +} + +const CONTRACT_CODE: &str = "60606040525b60008054600160a060020a03191633600160a060020a0316179055346001555b5b61011e806100356000396000f3006060604052361560465763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166383197ef08114604a5780638da5cb5b14605c575b5b5b005b3415605457600080fd5b60466095565b005b3415606657600080fd5b606c60d6565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b6000543373ffffffffffffffffffffffffffffffffffffffff9081169116141560d35760005473ffffffffffffffffffffffffffffffffffffffff16ff5b5b565b60005473ffffffffffffffffffffffffffffffffffffffff16815600a165627a7a7230582080eeb07bf95bf0cca20d03576cbb3a25de3bd0d1275c173d370dcc90ce23158d0029"; +const CONTRACT_INPUT: &str = "2df07fbaabbe40e3244445af30759352e348ec8bebd4dd75467a9f29ec55d98d6cf6c418de0e922b1c55be39587364b88224451e7901d10a4a2ee2eeab3cccf51c"; +const EXPECTED_POSITIONS: [u8; 27] = [ + 0, 2, 4, 5, 6, 7, 9, 10, 15, 45, 47, 48, 49, 50, 55, 56, 57, 59, 60, 61, 66, 67, 69, 70, 71, + 72, 73, +]; +const EXPECTED_COSTS: [u32; 27] = [ + 3, 3, 12, 2, 3, 3, 10, 3, 3, 3, 3, 5, 3, 3, 3, 3, 3, 10, 3, 3, 3, 3, 10, 1, 1, 1, 0, +]; +const EXPECTED_OP_CODES: [u8; 27] = [ + 96, 96, 82, 54, 21, 96, 87, 99, 124, 96, 53, 4, 22, 99, 129, 20, 96, 87, 128, 99, 20, 96, 87, + 91, 91, 91, 0, +]; diff --git a/engine/Cargo.toml b/engine/Cargo.toml index fbd0497e4..9e7239871 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -49,6 +49,7 @@ std = ["borsh/std", "evm/std", "primitive-types/std", "rlp/std", "sha3/std", "et contract = ["aurora-engine-sdk/contract", "aurora-engine-precompiles/contract"] evm_bully = [] log = ["aurora-engine-sdk/log", "aurora-engine-precompiles/log"] +tracing = ["evm/tracing"] meta-call = [] error_refund = ["aurora-engine-precompiles/error_refund"] integration-test = ["log"]