mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat: add js tracer (#3100)
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
@ -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"]
|
||||
|
||||
1
crates/revm/revm-inspectors/src/tracing/js/bigint.js
Normal file
1
crates/revm/revm-inspectors/src/tracing/js/bigint.js
Normal file
File diff suppressed because one or more lines are too long
797
crates/revm/revm-inspectors/src/tracing/js/bindings.rs
Normal file
797
crates/revm/revm-inspectors/src/tracing/js/bindings.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
163
crates/revm/revm-inspectors/src/tracing/js/builtins.rs
Normal file
163
crates/revm/revm-inspectors/src/tracing/js/builtins.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
549
crates/revm/revm-inspectors/src/tracing/js/mod.rs
Normal file
549
crates/revm/revm-inspectors/src/tracing/js/mod.rs
Normal 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),
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user