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

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> {