diff --git a/Cargo.lock b/Cargo.lock index 2fdf91d..f270f07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -735,12 +735,14 @@ dependencies = [ "ethers", "eyre", "function_name", + "hex", "lazy_static", "libc", "libloading", "parking_lot", "rustc-host", "serde", + "serde_json", "sneks", "tokio", ] diff --git a/replay/Cargo.toml b/replay/Cargo.toml index 6f9487c..f205eff 100644 --- a/replay/Cargo.toml +++ b/replay/Cargo.toml @@ -17,6 +17,7 @@ clap.workspace = true ethers.workspace = true eyre.workspace = true function_name.workspace = true +hex.workspace = true lazy_static.workspace = true libc.workspace = true libloading.workspace = true @@ -25,3 +26,6 @@ rustc-host.workspace = true serde = { version = "1.0.203", features = ["derive"] } sneks.workspace = true tokio.workspace = true + +[dev-dependencies] +serde_json.workspace = true diff --git a/replay/src/main.rs b/replay/src/main.rs index 79fd1da..9303553 100644 --- a/replay/src/main.rs +++ b/replay/src/main.rs @@ -48,15 +48,8 @@ enum Subcommands { #[derive(Args, Clone, Debug)] struct ReplayArgs { - /// RPC endpoint. - #[arg(short, long, default_value = "http://localhost:8547")] - endpoint: String, - /// Tx to replay. - #[arg(short, long)] - tx: TxHash, - /// Project path. - #[arg(short, long, default_value = ".")] - project: PathBuf, + #[command(flatten)] + trace: TraceArgs, /// Whether to use stable Rust. Note that nightly is needed to expand macros. #[arg(short, long)] stable_rust: bool, @@ -76,6 +69,9 @@ struct TraceArgs { /// Project path. #[arg(short, long, default_value = ".")] project: PathBuf, + /// If set, use the native tracer instead of the JavaScript one. Notice the native tracer might not be available in the node. + #[arg(short, long, default_value_t = false)] + use_native_tracer: bool, } fn main() -> Result<()> { @@ -106,7 +102,7 @@ async fn main_impl(args: Opts) -> Result<()> { async fn trace(args: TraceArgs) -> Result<()> { let provider = sys::new_provider(&args.endpoint)?; - let trace = Trace::new(provider, args.tx).await?; + let trace = Trace::new(provider, args.tx, args.use_native_tracer).await?; println!("{}", trace.json); Ok(()) } @@ -144,11 +140,11 @@ async fn replay(args: ReplayArgs) -> Result<()> { bail!("failed to exec gdb {:?}", err); } - let provider = sys::new_provider(&args.endpoint)?; - let trace = Trace::new(provider, args.tx).await?; + let provider = sys::new_provider(&args.trace.endpoint)?; + let trace = Trace::new(provider, args.trace.tx, args.trace.use_native_tracer).await?; - build_so(&args.project)?; - let so = find_so(&args.project)?; + build_so(&args.trace.project)?; + let so = find_so(&args.trace.project)?; // TODO: don't assume the contract is top-level let args_len = trace.tx.input.len(); diff --git a/replay/src/query.js b/replay/src/query.js index 43802f2..79db98e 100644 --- a/replay/src/query.js +++ b/replay/src/query.js @@ -1,17 +1,41 @@ { "hostio": function(info) { + info.args = toHex(info.args); + info.outs = toHex(info.outs); if (this.nests.includes(info.name)) { - info.info = this.open.pop(); + Object.assign(info, this.open.pop()); + info.name = info.name.substring(4) // remove evm_ } this.open.push(info); }, "enter": function(frame) { let inner = []; + let name = ""; + switch (frame.getType()) { + case "CALL": + name = "evm_call_contract"; + break; + case "DELEGATECALL": + name = "evm_delegate_call_contract"; + break; + case "STATICCALL": + name = "evm_static_call_contract"; + break; + case "CREATE": + name = "evm_create1"; + break; + case "CREATE2": + name = "evm_create2"; + break; + case "SELFDESTRUCT": + name = "evm_self_destruct"; + break; + } this.open.push({ - address: frame.getTo(), + address: toHex(frame.getTo()), steps: inner, + name: name, }); - this.stack.push(this.open); // save where we were this.open = inner; }, diff --git a/replay/src/trace.rs b/replay/src/trace.rs index f7b9bb2..04f2a5c 100644 --- a/replay/src/trace.rs +++ b/replay/src/trace.rs @@ -10,7 +10,7 @@ use ethers::{ types::{GethDebugTracerType, GethDebugTracingOptions, GethTrace, Transaction}, utils::__serde_json::{from_value, Value}, }; -use eyre::{bail, Result}; +use eyre::{bail, OptionExt, Result, WrapErr}; use serde::{Deserialize, Serialize}; use sneks::SimpleSnakeNames; use std::{collections::VecDeque, mem}; @@ -23,7 +23,11 @@ pub struct Trace { } impl Trace { - pub async fn new(provider: Provider, tx: TxHash) -> Result { + pub async fn new( + provider: Provider, + tx: TxHash, + use_native_tracer: bool, + ) -> Result { let hash = tx.0.into(); let Some(receipt) = provider.get_transaction_receipt(hash).await? else { @@ -33,7 +37,11 @@ impl Trace { bail!("failed to get tx data: {}", hash) }; - let query = include_str!("query.js"); + let query = if use_native_tracer { + "stylusTracer" + } else { + include_str!("query.js") + }; let tracer = GethDebugTracingOptions { tracer: Some(GethDebugTracerType::JsTracer(query.to_owned())), ..GethDebugTracingOptions::default() @@ -76,7 +84,7 @@ pub struct ActivationTraceFrame { address: Value, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TraceFrame { steps: Vec, address: Option
, @@ -117,45 +125,48 @@ impl TraceFrame { get_typed!(keys, Number, $name).as_u64().unwrap() }; } + macro_rules! get_hex { + ($name:expr) => {{ + let data = get_typed!(keys, String, $name); + let data = data + .strip_prefix("0x") + .ok_or_eyre(concat!($name, " does not contain 0x prefix"))?; + hex::decode(data) + .wrap_err(concat!("failed to parse ", $name))? + .into_boxed_slice() + }}; + } let name = get_typed!(keys, String, "name"); - let args = get_typed!(keys, Array, "args"); - let outs = get_typed!(keys, Array, "outs"); - - let mut args = args.as_slice(); - let mut outs = outs.as_slice(); + let mut args = get_hex!("args"); + let mut outs = get_hex!("outs"); let start_ink = get_int!("startInk"); let end_ink = get_int!("endInk"); - fn read_data(values: &[Value]) -> Result> { - let mut vec = vec![]; - for value in values { - let Value::Number(byte) = value else { - bail!("expected a byte but found {value}"); - }; - let byte = byte.as_u64().unwrap(); - if byte > 255 { - bail!("expected a byte but found {byte}"); - }; - vec.push(byte as u8); - } - Ok(vec.into_boxed_slice()) - } - macro_rules! read_data { ($src:ident) => {{ - let data = read_data(&$src)?; - $src = &[]; + let data = $src; + $src = Box::new([]); data }}; } macro_rules! read_ty { ($src:ident, $ty:ident, $conv:expr) => {{ let size = mem::size_of::<$ty>(); - let data = read_data(&$src[..size])?; - $src = &$src[size..]; - $conv(&data[..]) + let len = $src.len(); + if size > len { + bail!( + "parse {}: want {} bytes; got {}", + stringify!($src), + size, + len + ); + } + let (left, right) = $src.split_at(size); + let result = $conv(left); + $src = right.to_vec().into_boxed_slice(); + result }}; } macro_rules! read_string { @@ -208,16 +219,9 @@ impl TraceFrame { macro_rules! frame { () => {{ - let mut info = get_typed!(keys, Object, "info"); - - // geth uses the pattern { "0": Number, "1": Number, ... } - let address = get_typed!(info, Object, "address"); - let mut address: Vec<_> = address.into_iter().collect(); - address.sort_by_key(|x| x.0.parse::().unwrap()); - let address: Vec<_> = address.into_iter().map(|x| x.1).collect(); - let address = Address::from_slice(&*read_data(&address)?); - - let steps = info.remove("steps").unwrap(); + let address = get_hex!("address"); + let address = Address::from_slice(&address); + let steps = keys.remove("steps").unwrap(); TraceFrame::parse_frame(Some(address), steps)? }}; } @@ -411,7 +415,16 @@ impl TraceFrame { "console_log" => ConsoleLog { text: read_string!(args), }, - x => todo!("Missing hostio details {x}"), + x => { + if x.starts_with("evm_") { + EVMCall { + name: x.to_owned(), + frame: frame!(), + } + } else { + todo!("Missing hostio details {x}") + } + } }; assert!(args.is_empty(), "{name}"); @@ -427,7 +440,7 @@ impl TraceFrame { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Hostio { pub kind: HostioKind, pub start_ink: u64, @@ -435,7 +448,7 @@ pub struct Hostio { } #[allow(dead_code)] -#[derive(Clone, Debug, SimpleSnakeNames)] +#[derive(Clone, Debug, Eq, PartialEq, SimpleSnakeNames)] pub enum HostioKind { UserEntrypoint { args_len: u32, @@ -624,6 +637,10 @@ pub enum HostioKind { ReturnDataSize { size: u32, }, + EVMCall { + name: String, + frame: TraceFrame, + }, } #[derive(Debug)] @@ -678,3 +695,133 @@ impl FrameReader { } } } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{address, fixed_bytes}; + + #[test] + fn parse_simple() { + let trace = r#" + [ + { + "name": "storage_load_bytes32", + "args": "0xfafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafa", + "outs": "0xebebebebebebebebebebebebebebebebebebebebebebebebebebebebebebebeb", + "startInk": 1000, + "endInk": 900 + } + ]"#; + let json = serde_json::from_str(trace).expect("failed to parse json"); + let top_frame = TraceFrame::parse_frame(None, json).expect("failed to parse frame"); + assert_eq!( + top_frame.steps, + vec![Hostio { + kind: HostioKind::StorageLoadBytes32 { + key: fixed_bytes!( + "fafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafa" + ), + value: fixed_bytes!( + "ebebebebebebebebebebebebebebebebebebebebebebebebebebebebebebebeb" + ), + }, + start_ink: 1000, + end_ink: 900, + },] + ); + } + + #[test] + fn parse_call() { + let trace = r#" + [ + { + "name": "call_contract", + "args": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead00000000000000ff000000000000000000000000000000000000000000000000000000000000ffffbeef", + "outs": "0x0000000f00", + "startInk": 1000, + "endInk": 500, + "address": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead", + "steps": [ + { + "name": "user_entrypoint", + "args": "0x00000001", + "outs": "0x", + "startInk": 900, + "endInk": 600 + } + ] + } + ]"#; + let json = serde_json::from_str(trace).expect("failed to parse json"); + let top_frame = TraceFrame::parse_frame(None, json).expect("failed to parse frame"); + assert_eq!( + top_frame.steps, + vec![Hostio { + kind: HostioKind::CallContract { + address: address!("deaddeaddeaddeaddeaddeaddeaddeaddeaddead"), + data: Box::new([0xbe, 0xef]), + gas: 255, + value: U256::from(65535), + outs_len: 15, + status: 0, + frame: TraceFrame { + steps: vec![Hostio { + kind: HostioKind::UserEntrypoint { args_len: 1 }, + start_ink: 900, + end_ink: 600, + },], + address: Some(address!("deaddeaddeaddeaddeaddeaddeaddeaddeaddead"),), + }, + }, + start_ink: 1000, + end_ink: 500, + },], + ); + } + + #[test] + fn parse_evm_call() { + let trace = r#" + [ + { + "name": "evm_call_contract", + "args": "0x", + "outs": "0x", + "startInk": 0, + "endInk": 0, + "address": "0x457b1ba688e9854bdbed2f473f7510c476a3da09", + "steps": [ + { + "name": "user_entrypoint", + "args": "0x00000001", + "outs": "0x", + "startInk": 0, + "endInk": 0 + } + ] + } + ]"#; + let json = serde_json::from_str(trace).expect("failed to parse json"); + let top_frame = TraceFrame::parse_frame(None, json).expect("failed to parse frame"); + assert_eq!( + top_frame.steps, + vec![Hostio { + kind: HostioKind::EVMCall { + name: String::from("evm_call_contract"), + frame: TraceFrame { + steps: vec![Hostio { + kind: HostioKind::UserEntrypoint { args_len: 1 }, + start_ink: 0, + end_ink: 0, + },], + address: Some(address!("457b1ba688e9854bdbed2f473f7510c476a3da09")), + }, + }, + start_ink: 0, + end_ink: 0, + },], + ); + } +}