feat: add js tracer (#3100)

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
Matthias Seitz
2023-06-14 14:42:00 +02:00
committed by GitHub
parent 76302d945c
commit cf10590e4a
11 changed files with 2364 additions and 237 deletions

795
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,3 +18,14 @@ revm = { workspace = true }
hashbrown = "0.13"
serde = { workspace = true, features = ["derive"] }
thiserror = {version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
# js-tracing-inspector
boa_engine = { git = "https://github.com/boa-dev/boa", optional = true }
boa_gc = { git = "https://github.com/boa-dev/boa", optional = true }
tokio = { version = "1", features = ["sync"], optional = true }
[features]
default = ["js-tracer"]
js-tracer = ["boa_engine", "boa_gc", "tokio","thiserror", "serde_json"]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,797 @@
//! Type bindings for js tracing inspector
use crate::tracing::{
js::{
builtins::{
address_to_buf, bytes_to_address, bytes_to_hash, from_buf, to_bigint, to_bigint_array,
to_buf, to_buf_value,
},
JsDbRequest,
},
types::CallKind,
};
use boa_engine::{
native_function::NativeFunction,
object::{builtins::JsArrayBuffer, FunctionObjectBuilder},
Context, JsArgs, JsError, JsNativeError, JsObject, JsResult, JsValue,
};
use boa_gc::{empty_trace, Finalize, Gc, Trace};
use reth_primitives::{bytes::Bytes, Account, Address, H256, KECCAK_EMPTY, U256};
use revm::{
interpreter::{
opcode::{PUSH0, PUSH32},
Memory, OpCode, Stack,
},
primitives::State,
};
use std::{borrow::Borrow, sync::mpsc::channel};
use tokio::sync::mpsc;
/// A macro that creates a native function that returns via [JsValue::from]
macro_rules! js_value_getter {
($value:ident, $ctx:ident) => {
FunctionObjectBuilder::new(
$ctx,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from($value))),
)
.length(0)
.build()
};
}
/// A macro that creates a native function that returns a captured JsValue
macro_rules! js_value_capture_getter {
($value:ident, $ctx:ident) => {
FunctionObjectBuilder::new(
$ctx,
NativeFunction::from_copy_closure_with_captures(
move |_this, _args, input, _ctx| Ok(JsValue::from(input.clone())),
$value,
),
)
.length(0)
.build()
};
}
/// The Log object that is passed to the javascript inspector.
#[derive(Debug)]
pub(crate) struct StepLog {
/// Stack before step execution
pub(crate) stack: StackObj,
/// Opcode to be executed
pub(crate) op: OpObj,
/// All allocated memory in a step
pub(crate) memory: MemoryObj,
/// Program counter before step execution
pub(crate) pc: u64,
/// Remaining gas before step execution
pub(crate) gas_remaining: u64,
/// Gas cost of step execution
pub(crate) cost: u64,
/// Call depth
pub(crate) depth: u64,
/// Gas refund counter before step execution
pub(crate) refund: u64,
/// returns information about the error if one occurred, otherwise returns undefined
pub(crate) error: Option<String>,
/// The contract object available to the js inspector
pub(crate) contract: Contract,
}
impl StepLog {
/// Converts the contract object into a js object
///
/// Caution: this expects a global property `bigint` to be present.
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let Self {
stack,
op,
memory,
pc,
gas_remaining: gas,
cost,
depth,
refund,
error,
contract,
} = self;
let obj = JsObject::default();
// fields
let op = op.into_js_object(context)?;
let memory = memory.into_js_object(context)?;
let stack = stack.into_js_object(context)?;
let contract = contract.into_js_object(context)?;
obj.set("op", op, false, context)?;
obj.set("memory", memory, false, context)?;
obj.set("stack", stack, false, context)?;
obj.set("contract", contract, false, context)?;
// methods
let error =
if let Some(error) = error { JsValue::from(error) } else { JsValue::undefined() };
let get_error = js_value_capture_getter!(error, context);
let get_pc = js_value_getter!(pc, context);
let get_gas = js_value_getter!(gas, context);
let get_cost = js_value_getter!(cost, context);
let get_refund = js_value_getter!(refund, context);
let get_depth = js_value_getter!(depth, context);
obj.set("getPc", get_pc, false, context)?;
obj.set("getError", get_error, false, context)?;
obj.set("getGas", get_gas, false, context)?;
obj.set("getCost", get_cost, false, context)?;
obj.set("getDepth", get_depth, false, context)?;
obj.set("getRefund", get_refund, false, context)?;
Ok(obj)
}
}
/// Represents the memory object
#[derive(Debug)]
pub(crate) struct MemoryObj(pub(crate) Memory);
impl MemoryObj {
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let obj = JsObject::default();
let len = self.0.len();
// TODO: add into data <https://github.com/bluealloy/revm/pull/516>
let value = to_buf(self.0.data().clone(), context)?;
let length = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| {
Ok(JsValue::from(len as u64))
}),
)
.length(0)
.build();
let slice = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
|_this, args, memory, ctx| {
let start = args.get_or_undefined(0).to_number(ctx)?;
let end = args.get_or_undefined(1).to_number(ctx)?;
if end < start || start < 0. {
return Err(JsError::from_native(JsNativeError::typ().with_message(
format!(
"tracer accessed out of bound memory: offset {start}, end {end}"
),
)))
}
let start = start as usize;
let end = end as usize;
let mut mem = memory.take()?;
let slice = mem.drain(start..end).collect::<Vec<u8>>();
to_buf_value(slice, ctx)
},
value.clone(),
),
)
.length(2)
.build();
let get_uint = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
|_this, args, memory, ctx| {
let offset_f64 = args.get_or_undefined(0).to_number(ctx)?;
let mut mem = memory.take()?;
let offset = offset_f64 as usize;
if mem.len() < offset+32 || offset_f64 < 0. {
return Err(JsError::from_native(
JsNativeError::typ().with_message(format!("tracer accessed out of bound memory: available {}, offset {}, size 32", mem.len(), offset))
));
}
let slice = mem.drain(offset..offset+32).collect::<Vec<u8>>();
to_buf_value(slice, ctx)
},
value
),
)
.length(1)
.build();
obj.set("slice", slice, false, context)?;
obj.set("getUint", get_uint, false, context)?;
obj.set("length", length, false, context)?;
Ok(obj)
}
}
/// Represents the opcode object
#[derive(Debug)]
pub(crate) struct OpObj(pub(crate) OpCode);
impl OpObj {
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let obj = JsObject::default();
let value = self.0.u8();
let is_push = (PUSH0..=PUSH32).contains(&value);
let to_number = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from(value))),
)
.length(0)
.build();
let is_push = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from(is_push))),
)
.length(0)
.build();
let to_string = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| {
let s = OpCode::try_from_u8(value).expect("invalid opcode").to_string();
Ok(JsValue::from(s))
}),
)
.length(0)
.build();
obj.set("toNumber", to_number, false, context)?;
obj.set("toString", to_string, false, context)?;
obj.set("isPush", is_push, false, context)?;
Ok(obj)
}
}
/// Represents the stack object
#[derive(Debug, Clone)]
pub(crate) struct StackObj(pub(crate) Stack);
impl StackObj {
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let obj = JsObject::default();
let stack = self.0;
let len = stack.len();
let stack_arr = to_bigint_array(stack.data(), context)?;
let length = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, _ctx| Ok(JsValue::from(len))),
)
.length(0)
.build();
let peek = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, stack_arr, ctx| {
let idx_f64 = args.get_or_undefined(0).to_number(ctx)?;
let idx = idx_f64 as usize;
if len <= idx || idx_f64 < 0. {
return Err(JsError::from_native(JsNativeError::typ().with_message(
format!(
"tracer accessed out of bound stack: size {len}, index {idx_f64}"
),
)))
}
stack_arr.get(idx as u64, ctx)
},
stack_arr,
),
)
.length(1)
.build();
obj.set("length", length, false, context)?;
obj.set("peek", peek, false, context)?;
Ok(obj)
}
}
/// Represents the contract object
#[derive(Debug, Clone, Default)]
pub(crate) struct Contract {
pub(crate) caller: Address,
pub(crate) contract: Address,
pub(crate) value: U256,
pub(crate) input: Bytes,
}
impl Contract {
/// Converts the contract object into a js object
///
/// Caution: this expects a global property `bigint` to be present.
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let Contract { caller, contract, value, input } = self;
let obj = JsObject::default();
let get_caller = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, ctx| {
to_buf_value(caller.as_bytes().to_vec(), ctx)
}),
)
.length(0)
.build();
let get_address = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, ctx| {
to_buf_value(contract.as_bytes().to_vec(), ctx)
}),
)
.length(0)
.build();
let get_value = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, _args, ctx| to_bigint(value, ctx)),
)
.length(0)
.build();
let input = to_buf_value(input.to_vec(), context)?;
let get_input = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, _args, input, _ctx| Ok(input.clone()),
input,
),
)
.length(0)
.build();
obj.set("getCaller", get_caller, false, context)?;
obj.set("getAddress", get_address, false, context)?;
obj.set("getValue", get_value, false, context)?;
obj.set("getInput", get_input, false, context)?;
Ok(obj)
}
}
/// Represents the call frame object for exit functions
pub(crate) struct FrameResult {
pub(crate) gas_used: u64,
pub(crate) output: Bytes,
pub(crate) error: Option<String>,
}
impl FrameResult {
pub(crate) fn into_js_object(self, ctx: &mut Context<'_>) -> JsResult<JsObject> {
let Self { gas_used, output, error } = self;
let obj = JsObject::default();
let output = to_buf_value(output.to_vec(), ctx)?;
let get_output = FunctionObjectBuilder::new(
ctx,
NativeFunction::from_copy_closure_with_captures(
move |_this, _args, output, _ctx| Ok(output.clone()),
output,
),
)
.length(0)
.build();
let error = error.map(JsValue::from).unwrap_or_default();
let get_error = js_value_capture_getter!(error, ctx);
let get_gas_used = js_value_getter!(gas_used, ctx);
obj.set("getGasUsed", get_gas_used, false, ctx)?;
obj.set("getOutput", get_output, false, ctx)?;
obj.set("getError", get_error, false, ctx)?;
Ok(obj)
}
}
/// Represents the call frame object for enter functions
pub(crate) struct CallFrame {
pub(crate) contract: Contract,
pub(crate) kind: CallKind,
pub(crate) gas: u64,
}
impl CallFrame {
pub(crate) fn into_js_object(self, ctx: &mut Context<'_>) -> JsResult<JsObject> {
let CallFrame { contract: Contract { caller, contract, value, input }, kind, gas } = self;
let obj = JsObject::default();
let get_from = FunctionObjectBuilder::new(
ctx,
NativeFunction::from_copy_closure(move |_this, _args, ctx| {
to_buf_value(caller.as_bytes().to_vec(), ctx)
}),
)
.length(0)
.build();
let get_to = FunctionObjectBuilder::new(
ctx,
NativeFunction::from_copy_closure(move |_this, _args, ctx| {
to_buf_value(contract.as_bytes().to_vec(), ctx)
}),
)
.length(0)
.build();
let get_value = FunctionObjectBuilder::new(
ctx,
NativeFunction::from_copy_closure(move |_this, _args, ctx| to_bigint(value, ctx)),
)
.length(0)
.build();
let input = to_buf_value(input.to_vec(), ctx)?;
let get_input = FunctionObjectBuilder::new(
ctx,
NativeFunction::from_copy_closure_with_captures(
move |_this, _args, input, _ctx| Ok(input.clone()),
input,
),
)
.length(0)
.build();
let get_gas = js_value_getter!(gas, ctx);
let ty = kind.to_string();
let get_type = js_value_capture_getter!(ty, ctx);
obj.set("getFrom", get_from, false, ctx)?;
obj.set("getTo", get_to, false, ctx)?;
obj.set("getValue", get_value, false, ctx)?;
obj.set("getInput", get_input, false, ctx)?;
obj.set("getGas", get_gas, false, ctx)?;
obj.set("getType", get_type, false, ctx)?;
Ok(obj)
}
}
/// The `ctx` object that represents the context in which the transaction is executed.
pub(crate) struct EvmContext {
/// String, one of the two values CALL and CREATE
pub(crate) r#type: String,
/// Sender of the transaction
pub(crate) from: Address,
/// Target of the transaction
pub(crate) to: Option<Address>,
pub(crate) input: Bytes,
/// Gas limit
pub(crate) gas: u64,
/// Number, amount of gas used in executing the transaction (excludes txdata costs)
pub(crate) gas_used: u64,
/// Number, gas price configured in the transaction being executed
pub(crate) gas_price: u64,
/// Number, intrinsic gas for the transaction being executed
pub(crate) intrinsic_gas: u64,
/// big.int Amount to be transferred in wei
pub(crate) value: U256,
/// Number, block number
pub(crate) block: u64,
pub(crate) output: Bytes,
/// Number, block number
pub(crate) time: String,
// TODO more fields
pub(crate) block_hash: Option<H256>,
pub(crate) tx_index: Option<usize>,
pub(crate) tx_hash: Option<H256>,
}
impl EvmContext {
pub(crate) fn into_js_object(self, ctx: &mut Context<'_>) -> JsResult<JsObject> {
let Self {
r#type,
from,
to,
input,
gas,
gas_used,
gas_price,
intrinsic_gas,
value,
block,
output,
time,
block_hash,
tx_index,
tx_hash,
} = self;
let obj = JsObject::default();
// add properties
obj.set("type", r#type, false, ctx)?;
obj.set("from", address_to_buf(from, ctx)?, false, ctx)?;
if let Some(to) = to {
obj.set("to", address_to_buf(to, ctx)?, false, ctx)?;
} else {
obj.set("to", JsValue::null(), false, ctx)?;
}
obj.set("input", to_buf(input.to_vec(), ctx)?, false, ctx)?;
obj.set("gas", gas, false, ctx)?;
obj.set("gasUsed", gas_used, false, ctx)?;
obj.set("gasPrice", gas_price, false, ctx)?;
obj.set("intrinsicGas", intrinsic_gas, false, ctx)?;
obj.set("value", to_bigint(value, ctx)?, false, ctx)?;
obj.set("block", block, false, ctx)?;
obj.set("output", to_buf(output.to_vec(), ctx)?, false, ctx)?;
obj.set("time", time, false, ctx)?;
if let Some(block_hash) = block_hash {
obj.set("blockHash", to_buf(block_hash.as_bytes().to_vec(), ctx)?, false, ctx)?;
}
if let Some(tx_index) = tx_index {
obj.set("txIndex", tx_index as u64, false, ctx)?;
}
if let Some(tx_hash) = tx_hash {
obj.set("txHash", to_buf(tx_hash.as_bytes().to_vec(), ctx)?, false, ctx)?;
}
Ok(obj)
}
}
/// DB is the object that allows the js inspector to interact with the database.
pub(crate) struct EvmDb {
db: EvmDBInner,
}
impl EvmDb {
pub(crate) fn new(state: State, to_db: mpsc::Sender<JsDbRequest>) -> Self {
Self { db: EvmDBInner { state, to_db } }
}
}
impl EvmDb {
pub(crate) fn into_js_object(self, context: &mut Context<'_>) -> JsResult<JsObject> {
let obj = JsObject::default();
let db = Gc::new(self.db);
let exists = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, db, ctx| {
let val = args.get_or_undefined(0).clone();
let db: &EvmDBInner = db.borrow();
let acc = db.read_basic(val, ctx)?;
let exists = acc.is_some();
Ok(JsValue::from(exists))
},
db.clone(),
),
)
.length(1)
.build();
let get_balance = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, db, ctx| {
let val = args.get_or_undefined(0).clone();
let db: &EvmDBInner = db.borrow();
let acc = db.read_basic(val, ctx)?;
let balance = acc.map(|acc| acc.balance).unwrap_or_default();
to_bigint(balance, ctx)
},
db.clone(),
),
)
.length(1)
.build();
let get_nonce = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, db, ctx| {
let val = args.get_or_undefined(0).clone();
let db: &EvmDBInner = db.borrow();
let acc = db.read_basic(val, ctx)?;
let nonce = acc.map(|acc| acc.nonce).unwrap_or_default();
Ok(JsValue::from(nonce))
},
db.clone(),
),
)
.length(1)
.build();
let get_code = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, db, ctx| {
let val = args.get_or_undefined(0).clone();
let db: &EvmDBInner = db.borrow();
Ok(db.read_code(val, ctx)?.into())
},
db.clone(),
),
)
.length(1)
.build();
let get_state = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_this, args, db, ctx| {
let addr = args.get_or_undefined(0).clone();
let slot = args.get_or_undefined(1).clone();
let db: &EvmDBInner = db.borrow();
Ok(db.read_state(addr, slot, ctx)?.into())
},
db,
),
)
.length(2)
.build();
obj.set("getBalance", get_balance, false, context)?;
obj.set("getNonce", get_nonce, false, context)?;
obj.set("getCode", get_code, false, context)?;
obj.set("getState", get_state, false, context)?;
obj.set("exists", exists, false, context)?;
Ok(obj)
}
}
#[derive(Clone)]
struct EvmDBInner {
state: State,
to_db: mpsc::Sender<JsDbRequest>,
}
impl EvmDBInner {
fn read_basic(&self, address: JsValue, ctx: &mut Context<'_>) -> JsResult<Option<Account>> {
let buf = from_buf(address, ctx)?;
let address = bytes_to_address(buf);
if let Some(acc) = self.state.get(&address) {
return Ok(Some(Account {
nonce: acc.info.nonce,
balance: acc.info.balance,
bytecode_hash: Some(acc.info.code_hash),
}))
}
let (tx, rx) = channel();
if self.to_db.try_send(JsDbRequest::Basic { address, resp: tx }).is_err() {
return Err(JsError::from_native(
JsNativeError::error()
.with_message(format!("Failed to read address {address:?} from database",)),
))
}
match rx.recv() {
Ok(Ok(maybe_acc)) => Ok(maybe_acc),
_ => Err(JsError::from_native(
JsNativeError::error()
.with_message(format!("Failed to read address {address:?} from database",)),
)),
}
}
fn read_code(&self, address: JsValue, ctx: &mut Context<'_>) -> JsResult<JsArrayBuffer> {
let acc = self.read_basic(address, ctx)?;
let code_hash = acc.and_then(|acc| acc.bytecode_hash).unwrap_or(KECCAK_EMPTY);
if code_hash == KECCAK_EMPTY {
return JsArrayBuffer::new(0, ctx)
}
let (tx, rx) = channel();
if self.to_db.try_send(JsDbRequest::Code { code_hash, resp: tx }).is_err() {
return Err(JsError::from_native(
JsNativeError::error()
.with_message(format!("Failed to read code hash {code_hash:?} from database",)),
))
}
let code = match rx.recv() {
Ok(Ok(code)) => code,
_ => {
return Err(JsError::from_native(JsNativeError::error().with_message(format!(
"Failed to read code hash {code_hash:?} from database",
))))
}
};
to_buf(code.to_vec(), ctx)
}
fn read_state(
&self,
address: JsValue,
slot: JsValue,
ctx: &mut Context<'_>,
) -> JsResult<JsArrayBuffer> {
let buf = from_buf(address, ctx)?;
let address = bytes_to_address(buf);
let buf = from_buf(slot, ctx)?;
let slot = bytes_to_hash(buf);
let (tx, rx) = channel();
if self.to_db.try_send(JsDbRequest::StorageAt { address, index: slot, resp: tx }).is_err() {
return Err(JsError::from_native(JsNativeError::error().with_message(format!(
"Failed to read state for {address:?} at {slot:?} from database",
))))
}
let value = match rx.recv() {
Ok(Ok(value)) => value,
_ => {
return Err(JsError::from_native(JsNativeError::error().with_message(format!(
"Failed to read state for {address:?} at {slot:?} from database",
))))
}
};
let value: H256 = value.into();
to_buf(value.as_bytes().to_vec(), ctx)
}
}
impl Finalize for EvmDBInner {}
unsafe impl Trace for EvmDBInner {
empty_trace!();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tracing::js::builtins::BIG_INT_JS;
use boa_engine::{object::builtins::JsArrayBuffer, property::Attribute, Source};
#[test]
fn test_contract() {
let mut ctx = Context::default();
let contract = Contract {
caller: Address::zero(),
contract: Address::zero(),
value: U256::from(1337u64),
input: vec![0x01, 0x02, 0x03].into(),
};
let big_int = ctx.eval(Source::from_bytes(BIG_INT_JS.as_bytes())).unwrap();
ctx.register_global_property("bigint", big_int, Attribute::all()).unwrap();
let obj = contract.clone().into_js_object(&mut ctx).unwrap();
let s = r#"({
call: function(contract) { return contract.getCaller(); },
value: function(contract) { return contract.getValue(); },
input: function(contract) { return contract.getInput(); }
})"#;
let eval_obj = ctx.eval(Source::from_bytes(s.as_bytes())).unwrap();
let call = eval_obj.as_object().unwrap().get("call", &mut ctx).unwrap();
let res = call
.as_callable()
.unwrap()
.call(&JsValue::undefined(), &[obj.clone().into()], &mut ctx)
.unwrap();
assert!(res.is_object());
assert!(res.as_object().unwrap().is_array_buffer());
let call = eval_obj.as_object().unwrap().get("value", &mut ctx).unwrap();
let res = call
.as_callable()
.unwrap()
.call(&JsValue::undefined(), &[obj.clone().into()], &mut ctx)
.unwrap();
assert_eq!(
res.to_string(&mut ctx).unwrap().to_std_string().unwrap(),
contract.value.to_string()
);
let call = eval_obj.as_object().unwrap().get("input", &mut ctx).unwrap();
let res = call
.as_callable()
.unwrap()
.call(&JsValue::undefined(), &[obj.into()], &mut ctx)
.unwrap();
let buffer = JsArrayBuffer::from_object(res.as_object().unwrap().clone()).unwrap();
let input = buffer.take().unwrap();
assert_eq!(input, contract.input);
}
}

View File

@ -0,0 +1,163 @@
//! Builtin functions
use boa_engine::{
object::builtins::{JsArray, JsArrayBuffer},
property::Attribute,
Context, JsArgs, JsError, JsNativeError, JsResult, JsString, JsValue, NativeFunction, Source,
};
use reth_primitives::{hex, Address, H256, U256};
/// bigIntegerJS is the minified version of <https://github.com/peterolson/BigInteger.js>.
pub(crate) const BIG_INT_JS: &str = include_str!("bigint.js");
/// Registers all the builtin functions and global bigint property
pub(crate) fn register_builtins(ctx: &mut Context<'_>) -> JsResult<()> {
let big_int = ctx.eval(Source::from_bytes(BIG_INT_JS.as_bytes()))?;
ctx.register_global_property("bigint", big_int, Attribute::all())?;
ctx.register_global_builtin_callable("toHex", 1, NativeFunction::from_fn_ptr(to_hex))?;
// TODO: register toWord, toAddress toContract toContract2 isPrecompiled slice
Ok(())
}
/// Converts an array, hex string or Uint8Array to a []byte
pub(crate) fn from_buf(val: JsValue, context: &mut Context<'_>) -> JsResult<Vec<u8>> {
if let Some(obj) = val.as_object().cloned() {
if obj.is_array_buffer() {
let buf = JsArrayBuffer::from_object(obj)?;
return buf.take()
} else if obj.is_string() {
let js_string = obj.borrow().as_string().unwrap();
return hex_decode_js_string(js_string)
} else if obj.is_array() {
let array = JsArray::from_object(obj)?;
let len = array.length(context)?;
let mut buf = Vec::with_capacity(len as usize);
for i in 0..len {
let val = array.get(i, context)?;
buf.push(val.to_number(context)? as u8);
}
return Ok(buf)
}
}
Err(JsError::from_native(JsNativeError::typ().with_message("invalid buffer type")))
}
/// Create a new array buffer from the address' bytes.
pub(crate) fn address_to_buf(addr: Address, context: &mut Context<'_>) -> JsResult<JsArrayBuffer> {
to_buf(addr.0.to_vec(), context)
}
/// Create a new array buffer from byte block.
pub(crate) fn to_buf(bytes: Vec<u8>, context: &mut Context<'_>) -> JsResult<JsArrayBuffer> {
JsArrayBuffer::from_byte_block(bytes, context)
}
/// Create a new array buffer object from byte block.
pub(crate) fn to_buf_value(bytes: Vec<u8>, context: &mut Context<'_>) -> JsResult<JsValue> {
Ok(to_buf(bytes, context)?.into())
}
/// Create a new array buffer object from byte block.
pub(crate) fn to_bigint_array(items: &[U256], ctx: &mut Context<'_>) -> JsResult<JsArray> {
let arr = JsArray::new(ctx);
let bigint = ctx.global_object().get("bigint", ctx)?;
if !bigint.is_callable() {
return Err(JsError::from_native(
JsNativeError::typ().with_message("global object bigint is not callable"),
))
}
let bigint = bigint.as_callable().unwrap();
for item in items {
let val = bigint.call(&JsValue::undefined(), &[JsValue::from(item.to_string())], ctx)?;
arr.push(val, ctx)?;
}
Ok(arr)
}
/// Converts a buffer type to an address.
///
/// If the buffer is larger than the address size, it will be cropped from the left
pub(crate) fn bytes_to_address(buf: Vec<u8>) -> Address {
let mut address = Address::default();
let mut buf = &buf[..];
let address_len = address.0.len();
if buf.len() > address_len {
// crop from left
buf = &buf[buf.len() - address.0.len()..];
}
let address_slice = &mut address.0[address_len - buf.len()..];
address_slice.copy_from_slice(buf);
address
}
/// Converts a buffer type to a hash.
///
/// If the buffer is larger than the hash size, it will be cropped from the left
pub(crate) fn bytes_to_hash(buf: Vec<u8>) -> H256 {
let mut hash = H256::default();
let mut buf = &buf[..];
let hash_len = hash.0.len();
if buf.len() > hash_len {
// crop from left
buf = &buf[buf.len() - hash.0.len()..];
}
let hash_slice = &mut hash.0[hash_len - buf.len()..];
hash_slice.copy_from_slice(buf);
hash
}
/// Converts a U256 to a bigint using the global bigint property
pub(crate) fn to_bigint(value: U256, ctx: &mut Context<'_>) -> JsResult<JsValue> {
let bigint = ctx.global_object().get("bigint", ctx)?;
if !bigint.is_callable() {
return Ok(JsValue::undefined())
}
bigint.as_callable().unwrap().call(
&JsValue::undefined(),
&[JsValue::from(value.to_string())],
ctx,
)
}
/// Converts a buffer type to a hex string
pub(crate) fn to_hex(_: &JsValue, args: &[JsValue], ctx: &mut Context<'_>) -> JsResult<JsValue> {
let val = args.get_or_undefined(0).clone();
let buf = from_buf(val, ctx)?;
Ok(JsValue::from(hex::encode(buf)))
}
/// Decodes a hex decoded js-string
fn hex_decode_js_string(js_string: JsString) -> JsResult<Vec<u8>> {
match js_string.to_std_string() {
Ok(s) => match hex::decode(s.as_str()) {
Ok(data) => Ok(data),
Err(err) => Err(JsError::from_native(
JsNativeError::error().with_message(format!("invalid hex string {s}: {err}",)),
)),
},
Err(err) => Err(JsError::from_native(
JsNativeError::error()
.with_message(format!("invalid utf8 string {js_string:?}: {err}",)),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use boa_engine::Source;
#[test]
fn test_install_bigint() {
let mut ctx = Context::default();
let big_int = ctx.eval(Source::from_bytes(BIG_INT_JS.as_bytes())).unwrap();
let value = JsValue::from(100);
let result =
big_int.as_callable().unwrap().call(&JsValue::undefined(), &[value], &mut ctx).unwrap();
assert_eq!(result.to_string(&mut ctx).unwrap().to_std_string().unwrap(), "100");
}
}

View File

@ -0,0 +1,549 @@
//! Javascript inspector
use crate::tracing::{
js::{
bindings::{
CallFrame, Contract, EvmContext, EvmDb, FrameResult, MemoryObj, OpObj, StackObj,
StepLog,
},
builtins::register_builtins,
},
types::CallKind,
utils::get_create_address,
};
use boa_engine::{Context, JsError, JsObject, JsResult, JsValue, Source};
use reth_primitives::{bytes::Bytes, Account, Address, H256, U256};
use revm::{
interpreter::{
return_revert, CallInputs, CallScheme, CreateInputs, Gas, InstructionResult, Interpreter,
OpCode,
},
primitives::{Env, ExecutionResult, Output, ResultAndState, TransactTo, B160, B256},
Database, EVMData, Inspector,
};
use tokio::sync::mpsc;
pub(crate) mod bindings;
pub(crate) mod builtins;
/// A javascript inspector that will delegate inspector functions to javascript functions
///
/// See also <https://geth.ethereum.org/docs/developers/evm-tracing/custom-tracer#custom-javascript-tracing>
#[derive(Debug)]
pub struct JsInspector {
ctx: Context<'static>,
/// The javascript config provided to the inspector.
_config: JsValue,
/// The evaluated object that contains the inspector functions.
obj: JsObject,
/// The javascript function that will be called when the result is requested.
result_fn: JsObject,
fault_fn: JsObject,
/// EVM inspector hook functions
enter_fn: Option<JsObject>,
exit_fn: Option<JsObject>,
/// Executed before each instruction is executed.
step_fn: Option<JsObject>,
/// Keeps track of the current call stack.
call_stack: Vec<CallStackItem>,
/// sender half of a channel to communicate with the database service.
to_db_service: mpsc::Sender<JsDbRequest>,
}
impl JsInspector {
/// Creates a new inspector from a javascript code snipped that evaluates to an object with the
/// expected fields and a config object.
///
/// The object must have the following fields:
/// - `result`: a function that will be called when the result is requested.
/// - `fault`: a function that will be called when the transaction fails.
///
/// Optional functions are invoked during inspection:
/// - `enter`: a function that will be called when the execution enters a new call.
/// - `exit`: a function that will be called when the execution exits a call.
/// - `step`: a function that will be called when the execution steps to the next instruction.
///
/// This also accepts a sender half of a channel to communicate with the database service so the
/// DB can be queried from inside the inspector.
pub fn new(
code: String,
config: serde_json::Value,
to_db_service: mpsc::Sender<JsDbRequest>,
) -> Result<Self, JsInspectorError> {
// Instantiate the execution context
let mut ctx = Context::default();
register_builtins(&mut ctx)?;
// evaluate the code
let code = format!("({})", code);
let obj =
ctx.eval(Source::from_bytes(code.as_bytes())).map_err(JsInspectorError::EvalCode)?;
let obj = obj.as_object().cloned().ok_or(JsInspectorError::ExpectedJsObject)?;
// ensure all the fields are callables, if present
let result_fn = obj
.get("result", &mut ctx)?
.as_object()
.cloned()
.ok_or(JsInspectorError::ResultFunctionMissing)?;
if !result_fn.is_callable() {
return Err(JsInspectorError::ResultFunctionMissing)
}
let fault_fn = obj
.get("fault", &mut ctx)?
.as_object()
.cloned()
.ok_or(JsInspectorError::ResultFunctionMissing)?;
if !result_fn.is_callable() {
return Err(JsInspectorError::ResultFunctionMissing)
}
let enter_fn = obj.get("enter", &mut ctx)?.as_object().cloned().filter(|o| o.is_callable());
let exit_fn = obj.get("exit", &mut ctx)?.as_object().cloned().filter(|o| o.is_callable());
let step_fn = obj.get("step", &mut ctx)?.as_object().cloned().filter(|o| o.is_callable());
let config =
JsValue::from_json(&config, &mut ctx).map_err(JsInspectorError::InvalidJsonConfig)?;
if let Some(setup_fn) = obj.get("setup", &mut ctx)?.as_object() {
if !setup_fn.is_callable() {
return Err(JsInspectorError::SetupFunctionNotCallable)
}
// call setup()
setup_fn
.call(&(obj.clone().into()), &[config.clone()], &mut ctx)
.map_err(JsInspectorError::SetupCallFailed)?;
}
Ok(Self {
ctx,
_config: config,
obj,
result_fn,
fault_fn,
enter_fn,
exit_fn,
step_fn,
call_stack: Default::default(),
to_db_service,
})
}
/// Calls the result function and returns the result as [serde_json::Value].
///
/// Note: This is supposed to be called after the inspection has finished.
pub fn json_result(
&mut self,
res: ResultAndState,
env: &Env,
) -> Result<serde_json::Value, JsInspectorError> {
Ok(self.result(res, env)?.to_json(&mut self.ctx)?)
}
/// Calls the result function and returns the result.
pub fn result(&mut self, res: ResultAndState, env: &Env) -> Result<JsValue, JsInspectorError> {
let ResultAndState { result, state } = res;
let db = EvmDb::new(state, self.to_db_service.clone());
let gas_used = result.gas_used();
let mut to = None;
let mut output_bytes = None;
match result {
ExecutionResult::Success { output, .. } => match output {
Output::Call(out) => {
output_bytes = Some(out);
}
Output::Create(out, addr) => {
to = addr;
output_bytes = Some(out);
}
},
ExecutionResult::Revert { output, .. } => {
output_bytes = Some(output);
}
ExecutionResult::Halt { .. } => {}
};
let ctx = EvmContext {
r#type: match env.tx.transact_to {
TransactTo::Call(target) => {
to = Some(target);
"CALL"
}
TransactTo::Create(_) => "CREATE",
}
.to_string(),
from: env.tx.caller,
to,
input: env.tx.data.clone(),
gas: env.tx.gas_limit,
gas_used,
gas_price: env.tx.gas_price.try_into().unwrap_or(u64::MAX),
value: env.tx.value,
block: env.block.number.try_into().unwrap_or(u64::MAX),
output: output_bytes.unwrap_or_default(),
time: env.block.timestamp.to_string(),
// TODO: fill in the following fields
intrinsic_gas: 0,
block_hash: None,
tx_index: None,
tx_hash: None,
};
let ctx = ctx.into_js_object(&mut self.ctx)?;
let db = db.into_js_object(&mut self.ctx)?;
Ok(self.result_fn.call(
&(self.obj.clone().into()),
&[ctx.into(), db.into()],
&mut self.ctx,
)?)
}
fn try_fault(&mut self, step: StepLog, db: EvmDb) -> JsResult<()> {
let step = step.into_js_object(&mut self.ctx)?;
let db = db.into_js_object(&mut self.ctx)?;
self.fault_fn.call(&(self.obj.clone().into()), &[step.into(), db.into()], &mut self.ctx)?;
Ok(())
}
fn try_step(&mut self, step: StepLog, db: EvmDb) -> JsResult<()> {
if let Some(step_fn) = &self.step_fn {
let step = step.into_js_object(&mut self.ctx)?;
let db = db.into_js_object(&mut self.ctx)?;
step_fn.call(&(self.obj.clone().into()), &[step.into(), db.into()], &mut self.ctx)?;
}
Ok(())
}
fn try_enter(&mut self, frame: CallFrame, db: EvmDb) -> JsResult<()> {
if let Some(enter_fn) = &self.enter_fn {
let frame = frame.into_js_object(&mut self.ctx)?;
let db = db.into_js_object(&mut self.ctx)?;
enter_fn.call(&(self.obj.clone().into()), &[frame.into(), db.into()], &mut self.ctx)?;
}
Ok(())
}
fn try_exit(&mut self, frame: FrameResult, db: EvmDb) -> JsResult<()> {
if let Some(exit_fn) = &self.exit_fn {
let frame = frame.into_js_object(&mut self.ctx)?;
let db = db.into_js_object(&mut self.ctx)?;
exit_fn.call(&(self.obj.clone().into()), &[frame.into(), db.into()], &mut self.ctx)?;
}
Ok(())
}
/// Returns the currently active call
///
/// Panics: if there's no call yet
#[track_caller]
fn active_call(&self) -> &CallStackItem {
self.call_stack.last().expect("call stack is empty")
}
/// Pushes a new call to the stack
fn push_call(
&mut self,
address: Address,
data: Bytes,
value: U256,
kind: CallKind,
caller: Address,
gas_limit: u64,
) -> &CallStackItem {
let call = CallStackItem {
contract: Contract { caller, contract: address, value, input: data },
kind,
gas_limit,
};
self.call_stack.push(call);
self.active_call()
}
fn pop_call(&mut self) {
self.call_stack.pop();
}
}
impl<DB> Inspector<DB> for JsInspector
where
DB: Database,
{
fn initialize_interp(
&mut self,
_interp: &mut Interpreter,
_data: &mut EVMData<'_, DB>,
_is_static: bool,
) -> InstructionResult {
InstructionResult::Continue
}
fn step(
&mut self,
interp: &mut Interpreter,
data: &mut EVMData<'_, DB>,
_is_static: bool,
) -> InstructionResult {
if self.step_fn.is_none() {
return InstructionResult::Continue
}
let db = EvmDb::new(data.journaled_state.state.clone(), self.to_db_service.clone());
let pc = interp.program_counter();
let step = StepLog {
stack: StackObj(interp.stack.clone()),
op: OpObj(
OpCode::try_from_u8(interp.contract.bytecode.bytecode()[pc])
.expect("is valid opcode;"),
),
memory: MemoryObj(interp.memory.clone()),
pc: pc as u64,
gas_remaining: interp.gas.remaining(),
cost: interp.gas.spend(),
depth: data.journaled_state.depth(),
refund: interp.gas.refunded() as u64,
error: None,
contract: self.active_call().contract.clone(),
};
if self.try_step(step, db).is_err() {
return InstructionResult::Revert
}
InstructionResult::Continue
}
fn log(
&mut self,
_evm_data: &mut EVMData<'_, DB>,
_address: &B160,
_topics: &[B256],
_data: &Bytes,
) {
}
fn step_end(
&mut self,
interp: &mut Interpreter,
data: &mut EVMData<'_, DB>,
_is_static: bool,
eval: InstructionResult,
) -> InstructionResult {
if self.step_fn.is_none() {
return InstructionResult::Continue
}
if matches!(eval, return_revert!()) {
let db = EvmDb::new(data.journaled_state.state.clone(), self.to_db_service.clone());
let pc = interp.program_counter();
let step = StepLog {
stack: StackObj(interp.stack.clone()),
op: OpObj(
OpCode::try_from_u8(interp.contract.bytecode.bytecode()[pc])
.expect("is valid opcode;"),
),
memory: MemoryObj(interp.memory.clone()),
pc: pc as u64,
gas_remaining: interp.gas.remaining(),
cost: interp.gas.spend(),
depth: data.journaled_state.depth(),
refund: interp.gas.refunded() as u64,
error: Some(format!("{:?}", eval)),
contract: self.active_call().contract.clone(),
};
let _ = self.try_fault(step, db);
}
InstructionResult::Continue
}
fn call(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CallInputs,
_is_static: bool,
) -> (InstructionResult, Gas, Bytes) {
// determine correct `from` and `to` based on the call scheme
let (from, to) = match inputs.context.scheme {
CallScheme::DelegateCall | CallScheme::CallCode => {
(inputs.context.address, inputs.context.code_address)
}
_ => (inputs.context.caller, inputs.context.address),
};
let value = inputs.transfer.value;
self.push_call(
to,
inputs.input.clone(),
value,
inputs.context.scheme.into(),
from,
inputs.gas_limit,
);
if self.enter_fn.is_some() {
let call = self.active_call();
let frame = CallFrame {
contract: call.contract.clone(),
kind: call.kind,
gas: inputs.gas_limit,
};
let db = EvmDb::new(data.journaled_state.state.clone(), self.to_db_service.clone());
if let Err(err) = self.try_enter(frame, db) {
return (InstructionResult::Revert, Gas::new(0), err.to_string().into())
}
}
(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) {
if self.exit_fn.is_some() {
let frame_result =
FrameResult { gas_used: remaining_gas.spend(), output: out.clone(), error: None };
let db = EvmDb::new(data.journaled_state.state.clone(), self.to_db_service.clone());
if let Err(err) = self.try_exit(frame_result, db) {
return (InstructionResult::Revert, Gas::new(0), err.to_string().into())
}
}
self.pop_call();
(ret, remaining_gas, out)
}
fn create(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CreateInputs,
) -> (InstructionResult, Option<B160>, Gas, Bytes) {
let _ = data.journaled_state.load_account(inputs.caller, data.db);
let nonce = data.journaled_state.account(inputs.caller).info.nonce;
let address = get_create_address(inputs, nonce);
self.push_call(
address,
inputs.init_code.clone(),
inputs.value,
inputs.scheme.into(),
inputs.caller,
inputs.gas_limit,
);
if self.enter_fn.is_some() {
let call = self.active_call();
let frame =
CallFrame { contract: call.contract.clone(), kind: call.kind, gas: call.gas_limit };
let db = EvmDb::new(data.journaled_state.state.clone(), self.to_db_service.clone());
if let Err(err) = self.try_enter(frame, db) {
return (InstructionResult::Revert, None, Gas::new(0), err.to_string().into())
}
}
(InstructionResult::Continue, None, Gas::new(inputs.gas_limit), Bytes::default())
}
fn create_end(
&mut self,
data: &mut EVMData<'_, DB>,
_inputs: &CreateInputs,
ret: InstructionResult,
address: Option<B160>,
remaining_gas: Gas,
out: Bytes,
) -> (InstructionResult, Option<B160>, Gas, Bytes) {
if self.exit_fn.is_some() {
let frame_result =
FrameResult { gas_used: remaining_gas.spend(), output: out.clone(), error: None };
let db = EvmDb::new(data.journaled_state.state.clone(), self.to_db_service.clone());
if let Err(err) = self.try_exit(frame_result, db) {
return (InstructionResult::Revert, None, Gas::new(0), err.to_string().into())
}
}
self.pop_call();
(ret, address, remaining_gas, out)
}
fn selfdestruct(&mut self, _contract: B160, _target: B160) {
if self.enter_fn.is_some() {
let call = self.active_call();
let frame =
CallFrame { contract: call.contract.clone(), kind: call.kind, gas: call.gas_limit };
let db = EvmDb::new(Default::default(), self.to_db_service.clone());
let _ = self.try_enter(frame, db);
}
}
}
/// Request variants to be sent from the inspector to the database
#[derive(Debug, Clone)]
pub enum JsDbRequest {
/// Bindings for [Database::basic]
Basic {
/// The address of the account to be loaded
address: Address,
/// The response channel
resp: std::sync::mpsc::Sender<Result<Option<Account>, String>>,
},
/// Bindings for [Database::code_by_hash]
Code {
/// The code hash of the code to be loaded
code_hash: H256,
/// The response channel
resp: std::sync::mpsc::Sender<Result<Bytes, String>>,
},
/// Bindings for [Database::storage]
StorageAt {
/// The address of the account
address: Address,
/// Index of the storage slot
index: H256,
/// The response channel
resp: std::sync::mpsc::Sender<Result<U256, String>>,
},
}
/// Represents an active call
#[derive(Debug)]
struct CallStackItem {
contract: Contract,
kind: CallKind,
gas_limit: u64,
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum JsInspectorError {
#[error(transparent)]
JsError(#[from] JsError),
#[error("Failed to eval js code: {0}")]
EvalCode(JsError),
#[error("The evaluated code is not a JS object")]
ExpectedJsObject,
#[error("trace object must expose a function result()")]
ResultFunctionMissing,
#[error("trace object must expose a function fault()")]
FaultFunctionMissing,
#[error("setup object must be a function")]
SetupFunctionNotCallable,
#[error("Failed to call setup(): {0}")]
SetupCallFailed(JsError),
#[error("Invalid JSON config: {0}")]
InvalidJsonConfig(JsError),
}

View File

@ -27,6 +27,9 @@ pub use config::TracingInspectorConfig;
pub use fourbyte::FourByteInspector;
pub use opcount::OpcodeCountInspector;
#[cfg(feature = "js-tracer")]
pub mod js;
/// An inspector that collects call traces.
///
/// This [Inspector] can be hooked into the [EVM](revm::EVM) which then calls the inspector

View File

@ -10,6 +10,7 @@ use revm::{
pub type SubState<DB> = CacheDB<State<DB>>;
/// Wrapper around StateProvider that implements revm database trait
#[derive(Debug, Clone)]
pub struct State<DB: StateProvider>(pub DB);
impl<DB: StateProvider> State<DB> {

View File

@ -100,6 +100,7 @@ pub enum GethTraceFrame {
FourByteTracer(FourByteFrame),
CallTracer(CallFrame),
PreStateTracer(PreStateFrame),
JS(serde_json::Value),
}
impl From<DefaultFrame> for GethTraceFrame {
@ -241,6 +242,14 @@ impl GethDebugTracerConfig {
}
}
/// Returns the json config if this config is a JS tracer.
pub fn into_js_config(self) -> Option<serde_json::Value> {
match self {
GethDebugTracerConfig::JsTracer(cfg) => Some(cfg),
_ => None,
}
}
/// Returns the [PreStateConfig] if it is a call config.
pub fn into_pre_state_config(self) -> Option<PreStateConfig> {
match self {

View File

@ -1,7 +1,7 @@
use crate::{
eth::{
error::{EthApiError, EthResult},
revm_utils::{inspect, replay_transactions_until, EvmOverrides},
revm_utils::{inspect, prepare_call_env, replay_transactions_until, EvmOverrides},
EthTransactions, TransactionSource,
},
result::{internal_rpc_err, ToRpcResult},
@ -10,11 +10,16 @@ use crate::{
use async_trait::async_trait;
use jsonrpsee::core::RpcResult;
use reth_primitives::{Block, BlockId, BlockNumberOrTag, Bytes, TransactionSigned, H256};
use reth_provider::{BlockProviderIdExt, HeaderProvider, ReceiptProviderIdExt, StateProviderBox};
use reth_provider::{
BlockProviderIdExt, HeaderProvider, ReceiptProviderIdExt, StateProvider, StateProviderBox,
};
use reth_revm::{
database::{State, SubState},
env::tx_env_with_recovered,
tracing::{FourByteInspector, TracingInspector, TracingInspectorConfig},
tracing::{
js::{JsDbRequest, JsInspector},
FourByteInspector, TracingInspector, TracingInspectorConfig,
},
};
use reth_rlp::{Decodable, Encodable};
use reth_rpc_api::DebugApiServer;
@ -30,7 +35,8 @@ use reth_tasks::TaskSpawner;
use revm::primitives::Env;
use revm_primitives::{db::DatabaseCommit, BlockEnv, CfgEnv};
use std::{future::Future, sync::Arc};
use tokio::sync::{oneshot, AcquireError, OwnedSemaphorePermit};
use tokio::sync::{mpsc, oneshot, AcquireError, OwnedSemaphorePermit};
use tokio_stream::{wrappers::ReceiverStream, StreamExt};
/// `debug` API implementation.
///
@ -94,6 +100,7 @@ where
opts: GethDebugTracingOptions,
) -> EthResult<Vec<TraceResult>> {
// replay all transactions of the block
let this = self.clone();
self.inner.eth_api.with_state_at_block(at, move |state| {
let mut results = Vec::with_capacity(transactions.len());
let mut db = SubState::new(State::new(state));
@ -103,7 +110,8 @@ where
let tx = tx.into_ecrecovered().ok_or(BlockError::InvalidSignature)?;
let tx = tx_env_with_recovered(&tx);
let env = Env { cfg: cfg.clone(), block: block_env.clone(), tx };
let (result, state_changes) = trace_transaction(opts.clone(), env, &mut db)?;
let (result, state_changes) =
this.trace_transaction(opts.clone(), env, at, &mut db)?;
results.push(TraceResult::Success { result });
if transactions.peek().is_some() {
@ -204,11 +212,11 @@ where
// we need to get the state of the parent block because we're essentially replaying the
// block the transaction is included in
let state_at = block.parent_hash;
let state_at: BlockId = block.parent_hash.into();
let block_txs = block.body;
self.on_blocking_task(|this| async move {
this.inner.eth_api.with_state_at_block(state_at.into(), |state| {
this.inner.eth_api.with_state_at_block(state_at, |state| {
// configure env for the target transaction
let tx = transaction.into_recovered();
@ -223,7 +231,7 @@ where
)?;
let env = Env { cfg, block: block_env, tx: tx_env_with_recovered(&tx) };
trace_transaction(opts, env, &mut db).map(|(trace, _)| trace)
this.trace_transaction(opts, env, state_at, &mut db).map(|(trace, _)| trace)
})
})
.await
@ -302,8 +310,23 @@ where
}
GethDebugBuiltInTracerType::NoopTracer => Ok(NoopFrame::default().into()),
},
GethDebugTracerType::JsTracer(_) => {
Err(EthApiError::Unsupported("javascript tracers are unsupported."))
GethDebugTracerType::JsTracer(code) => {
let config = tracer_config.and_then(|c| c.into_js_config()).unwrap_or_default();
// for JS tracing we need to setup all async work before we can start tracing
// because JSTracer and all JS types are not Send
let (cfg, block_env, at) = self.inner.eth_api.evm_env_at(at).await?;
let state = self.inner.eth_api.state_at(at)?;
let mut db = SubState::new(State::new(state));
let env = prepare_call_env(cfg, block_env, call, &mut db, overrides)?;
let to_db_service = self.spawn_js_trace_service(at)?;
let mut inspector = JsInspector::new(code, config, to_db_service)?;
let (res, env) = inspect(db, env, &mut inspector)?;
let result = inspector.json_result(res, &env)?;
Ok(GethTraceFrame::JS(result))
}
}
}
@ -321,6 +344,149 @@ where
Ok(frame.into())
}
/// Executes the configured transaction with the environment on the given database.
///
/// Returns the trace frame and the state that got updated after executing the transaction.
///
/// Note: this does not apply any state overrides if they're configured in the `opts`.
fn trace_transaction(
&self,
opts: GethDebugTracingOptions,
env: Env,
at: BlockId,
db: &mut SubState<StateProviderBox<'_>>,
) -> EthResult<(GethTraceFrame, revm_primitives::State)> {
let GethDebugTracingOptions { config, tracer, tracer_config, .. } = opts;
if let Some(tracer) = tracer {
// valid matching config
if let Some(ref config) = tracer_config {
if !config.matches_tracer(&tracer) {
return Err(EthApiError::InvalidTracerConfig)
}
}
return match tracer {
GethDebugTracerType::BuiltInTracer(tracer) => match tracer {
GethDebugBuiltInTracerType::FourByteTracer => {
let mut inspector = FourByteInspector::default();
let (res, _) = inspect(db, env, &mut inspector)?;
return Ok((FourByteFrame::from(inspector).into(), res.state))
}
GethDebugBuiltInTracerType::CallTracer => {
// we validated the config above
let call_config =
tracer_config.and_then(|c| c.into_call_config()).unwrap_or_default();
let mut inspector = TracingInspector::new(
TracingInspectorConfig::from_geth_config(&config),
);
let (res, _) = inspect(db, env, &mut inspector)?;
let frame = inspector.into_geth_builder().geth_call_traces(call_config);
return Ok((frame.into(), res.state))
}
GethDebugBuiltInTracerType::PreStateTracer => {
Err(EthApiError::Unsupported("prestate tracer is unimplemented yet."))
}
GethDebugBuiltInTracerType::NoopTracer => {
Ok((NoopFrame::default().into(), Default::default()))
}
},
GethDebugTracerType::JsTracer(code) => {
let config = tracer_config.and_then(|c| c.into_js_config()).unwrap_or_default();
// we spawn the database service that will be used by the JS tracer
// TODO(mattsse) this is not quite accurate when tracing a block inside a
// transaction because the service needs access to the committed state changes
let to_db_service = self.spawn_js_trace_service(at)?;
let mut inspector = JsInspector::new(code, config, to_db_service)?;
let (res, env) = inspect(db, env, &mut inspector)?;
let state = res.state.clone();
let result = inspector.json_result(res, &env)?;
Ok((GethTraceFrame::JS(result), state))
}
}
}
// default structlog tracer
let inspector_config = TracingInspectorConfig::from_geth_config(&config);
let mut inspector = TracingInspector::new(inspector_config);
let (res, _) = inspect(db, env, &mut inspector)?;
let gas_used = res.result.gas_used();
let frame = inspector.into_geth_builder().geth_traces(gas_used, config);
Ok((frame.into(), res.state))
}
/// Spawns [Self::js_trace_db_service_task] on a new task and returns a channel to send requests
/// to it.
///
/// Note: This blocks until the service is ready to receive requests.
fn spawn_js_trace_service(&self, at: BlockId) -> EthResult<mpsc::Sender<JsDbRequest>> {
let (to_db_service, rx) = mpsc::channel(1);
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let this = self.clone();
self.inner
.task_spawner
.spawn(Box::pin(async move { this.js_trace_db_service_task(at, rx, ready_tx).await }));
// wait for initialization
ready_rx.recv().map_err(|_| {
EthApiError::InternalJsTracerError("js tracer initialization failed".to_string())
})??;
Ok(to_db_service)
}
/// A services that handles database requests issued from inside the JavaScript tracing engine.
async fn js_trace_db_service_task(
self,
at: BlockId,
rx: mpsc::Receiver<JsDbRequest>,
on_ready: std::sync::mpsc::Sender<EthResult<()>>,
) {
let state = match self.inner.eth_api.state_at(at) {
Ok(state) => {
let _ = on_ready.send(Ok(()));
state
}
Err(err) => {
let _ = on_ready.send(Err(err));
return
}
};
let mut stream = ReceiverStream::new(rx);
while let Some(req) = stream.next().await {
match req {
JsDbRequest::Basic { address, resp } => {
let acc = state.basic_account(address).map_err(|err| err.to_string());
let _ = resp.send(acc);
}
JsDbRequest::Code { code_hash, resp } => {
let code = state
.bytecode_by_hash(code_hash)
.map(|code| code.map(|c| c.bytecode.clone()).unwrap_or_default())
.map_err(|err| err.to_string());
let _ = resp.send(code);
}
JsDbRequest::StorageAt { address, index, resp } => {
let value = state
.storage(address, index)
.map(|val| val.unwrap_or_default())
.map_err(|err| err.to_string());
let _ = resp.send(value);
}
}
}
}
}
#[async_trait]
@ -486,70 +652,3 @@ struct DebugApiInner<Provider, Eth> {
/// The type that can spawn tasks which would otherwise block.
task_spawner: Box<dyn TaskSpawner>,
}
/// Executes the configured transaction with the environment on the given database.
///
/// Returns the trace frame and the state that got updated after executing the transaction.
///
/// Note: this does not apply any state overrides if they're configured in the `opts`.
fn trace_transaction(
opts: GethDebugTracingOptions,
env: Env,
db: &mut SubState<StateProviderBox<'_>>,
) -> EthResult<(GethTraceFrame, revm_primitives::State)> {
let GethDebugTracingOptions { config, tracer, tracer_config, .. } = opts;
if let Some(tracer) = tracer {
// valid matching config
if let Some(ref config) = tracer_config {
if !config.matches_tracer(&tracer) {
return Err(EthApiError::InvalidTracerConfig)
}
}
return match tracer {
GethDebugTracerType::BuiltInTracer(tracer) => match tracer {
GethDebugBuiltInTracerType::FourByteTracer => {
let mut inspector = FourByteInspector::default();
let (res, _) = inspect(db, env, &mut inspector)?;
return Ok((FourByteFrame::from(inspector).into(), res.state))
}
GethDebugBuiltInTracerType::CallTracer => {
// we validated the config above
let call_config =
tracer_config.and_then(|c| c.into_call_config()).unwrap_or_default();
let mut inspector =
TracingInspector::new(TracingInspectorConfig::from_geth_config(&config));
let (res, _) = inspect(db, env, &mut inspector)?;
let frame = inspector.into_geth_builder().geth_call_traces(call_config);
return Ok((frame.into(), res.state))
}
GethDebugBuiltInTracerType::PreStateTracer => {
Err(EthApiError::Unsupported("prestate tracer is unimplemented yet."))
}
GethDebugBuiltInTracerType::NoopTracer => {
Ok((NoopFrame::default().into(), Default::default()))
}
},
GethDebugTracerType::JsTracer(_) => {
Err(EthApiError::Unsupported("javascript tracers are unsupported."))
}
}
}
// default structlog tracer
let inspector_config = TracingInspectorConfig::from_geth_config(&config);
let mut inspector = TracingInspector::new(inspector_config);
let (res, _) = inspect(db, env, &mut inspector)?;
let gas_used = res.result.gas_used();
let frame = inspector.into_geth_builder().geth_traces(gas_used, config);
Ok((frame.into(), res.state))
}

View File

@ -3,6 +3,7 @@
use crate::result::{internal_rpc_err, invalid_params_rpc_err, rpc_err, rpc_error_with_code};
use jsonrpsee::{core::Error as RpcError, types::ErrorObject};
use reth_primitives::{abi::decode_revert_reason, Address, Bytes, U256};
use reth_revm::tracing::js::JsInspectorError;
use reth_rpc_types::{error::EthRpcErrorCode, BlockError};
use reth_transaction_pool::error::{InvalidPoolTransactionError, PoolError};
use revm::primitives::{EVMError, ExecutionResult, Halt, OutOfGasError};
@ -57,6 +58,9 @@ pub enum EthApiError {
/// Some feature is unsupported
#[error("unsupported")]
Unsupported(&'static str),
/// General purpose error for invalid params
#[error("{0}")]
InvalidParams(String),
/// When tracer config does not match the tracer
#[error("invalid tracer config")]
InvalidTracerConfig,
@ -69,6 +73,9 @@ pub enum EthApiError {
/// Error thrown when a spawned blocking task failed to deliver an anticipated response.
#[error("internal eth error")]
InternalEthError,
/// Internal Error thrown by the javascript tracer
#[error("{0}")]
InternalJsTracerError(String),
}
impl From<EthApiError> for ErrorObject<'static> {
@ -92,6 +99,8 @@ impl From<EthApiError> for ErrorObject<'static> {
rpc_error_with_code(EthRpcErrorCode::ResourceNotFound.code(), error.to_string())
}
EthApiError::Unsupported(msg) => internal_rpc_err(msg),
EthApiError::InternalJsTracerError(msg) => internal_rpc_err(msg),
EthApiError::InvalidParams(msg) => invalid_params_rpc_err(msg),
EthApiError::InvalidRewardPercentile(msg) => internal_rpc_err(msg.to_string()),
err @ EthApiError::InternalTracingError => internal_rpc_err(err.to_string()),
err @ EthApiError::InternalEthError => internal_rpc_err(err.to_string()),
@ -104,6 +113,16 @@ impl From<EthApiError> for RpcError {
RpcError::Call(error.into())
}
}
impl From<JsInspectorError> for EthApiError {
fn from(error: JsInspectorError) -> Self {
match error {
err @ JsInspectorError::JsError(_) => {
EthApiError::InternalJsTracerError(err.to_string())
}
err => EthApiError::InvalidParams(err.to_string()),
}
}
}
impl From<reth_interfaces::Error> for EthApiError {
fn from(error: reth_interfaces::Error) -> Self {