Skip to content

Commit

Permalink
feat(tracing/js): move try_step to step_end
Browse files Browse the repository at this point in the history
Signed-off-by: jsvisa <[email protected]>
  • Loading branch information
jsvisa committed Oct 7, 2024
1 parent f9481b9 commit 2a3f6cf
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 31 deletions.
2 changes: 2 additions & 0 deletions src/tracing/js/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ macro_rules! js_value_getter {
};
}

pub(crate) use js_value_getter;

/// A macro that creates a native function that returns a captured JsValue
macro_rules! js_value_capture_getter {
($value:ident, $ctx:ident) => {
Expand Down
93 changes: 62 additions & 31 deletions src/tracing/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
use crate::tracing::{
js::{
bindings::{
CallFrame, Contract, EvmDbRef, FrameResult, JsEvmContext, MemoryRef, StackRef, StepLog,
js_value_getter, CallFrame, Contract, EvmDbRef, FrameResult, JsEvmContext, MemoryRef,
StackRef, StepLog,
},
builtins::{register_builtins, to_serde_value, PrecompileList},
},
Expand All @@ -12,7 +13,10 @@ use crate::tracing::{
};
use alloy_primitives::{Address, Bytes, Log, U256};
pub use boa_engine::vm::RuntimeLimits;
use boa_engine::{js_string, Context, JsError, JsObject, JsResult, JsValue, Source};
use boa_engine::{
js_string, object::FunctionObjectBuilder, Context, JsError, JsObject, JsResult, JsValue,
NativeFunction, Source,
};
use revm::{
interpreter::{
return_revert, CallInputs, CallOutcome, CallScheme, CreateInputs, CreateOutcome, Gas,
Expand Down Expand Up @@ -72,6 +76,10 @@ pub struct JsInspector {
call_stack: Vec<CallStackItem>,
/// Marker to track whether the precompiles have been registered.
precompiles_registered: bool,
/// Represents the current step log that is being processed, initialized in the `step`
/// function, and be updated and used in the `step_end` function.
step: Option<JsObject>,
gas_remaining: u64,
}

impl JsInspector {
Expand Down Expand Up @@ -177,6 +185,8 @@ impl JsInspector {
step_fn,
call_stack: Default::default(),
precompiles_registered: false,
step: None,
gas_remaining: 0,
})
}

Expand Down Expand Up @@ -290,11 +300,16 @@ impl JsInspector {
Ok(())
}

fn try_step(&mut self, step: StepLog, db: EvmDbRef) -> JsResult<()> {
fn try_step(&mut self, step: JsObject, cost: u64, refund: u64, db: EvmDbRef) -> 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)?;
let ctx = &mut self.ctx;
let get_cost = js_value_getter!(cost, ctx);
let get_refund = js_value_getter!(refund, ctx);
step.set(js_string!("getCost"), get_cost, false, ctx)?;
step.set(js_string!("getRefund"), get_refund, false, ctx)?;

let db = db.into_js_object(ctx)?;
step_fn.call(&(self.obj.clone().into()), &[step.into(), db.into()], ctx)?;
}
Ok(())
}
Expand Down Expand Up @@ -388,53 +403,69 @@ where
return;
}

let (db, _db_guard) = EvmDbRef::new(&context.journaled_state.state, &context.db);

let (stack, _stack_guard) = StackRef::new(&interp.stack);
let (memory, _memory_guard) = MemoryRef::new(&interp.shared_memory);

// Create and store a new step log. The `cost` and `refund` values cannot be calculated yet,
// as they depend on the opcode execution. These values will be updated in `step_end`
// after the opcode has been executed, and then the `step` function will be invoked.
// Initialize `cost` and `refund` as placeholders for later updates.
let step = StepLog {
stack,
op: interp.current_opcode().into(),
memory,
pc: interp.program_counter() as u64,
gas_remaining: interp.gas.remaining(),
cost: interp.gas.spent(),
cost: 0,
depth: context.journaled_state.depth(),
refund: interp.gas.refunded() as u64,
refund: 0,
error: None,
contract: self.active_call().contract.clone(),
};

if self.try_step(step, db).is_err() {
interp.instruction_result = InstructionResult::Revert;
}
self.gas_remaining = interp.gas.remaining();
match step.into_js_object(&mut self.ctx) {
Ok(step) => self.step = Some(step),
Err(err) => interp.instruction_result = InstructionResult::Revert,
};
}

fn step_end(&mut self, interp: &mut Interpreter, context: &mut EvmContext<DB>) {
if self.step_fn.is_none() {
return;
}

if matches!(interp.instruction_result, return_revert!()) {
let (db, _db_guard) = EvmDbRef::new(&context.journaled_state.state, &context.db);
// Calculate the gas cost and refund after opcode execution
if let Some(step) = self.step.take() {
let cost = self.gas_remaining.saturating_sub(interp.gas.remaining());
let refund = interp.gas.refunded() as u64;

let (stack, _stack_guard) = StackRef::new(&interp.stack);
let (memory, _memory_guard) = MemoryRef::new(&interp.shared_memory);
let step = StepLog {
stack,
op: interp.current_opcode().into(),
memory,
pc: interp.program_counter() as u64,
gas_remaining: interp.gas.remaining(),
cost: interp.gas.spent(),
depth: context.journaled_state.depth(),
refund: interp.gas.refunded() as u64,
error: Some(format!("{:?}", interp.instruction_result)),
contract: self.active_call().contract.clone(),
};
let (db, _db_guard) = EvmDbRef::new(&context.journaled_state.state, &context.db);
if self.try_step(step, cost, refund, db).is_err() {
interp.instruction_result = InstructionResult::Revert;
}

let _ = self.try_fault(step, db);
}
if matches!(interp.instruction_result, return_revert!()) {
let (db, _db_guard) = EvmDbRef::new(&context.journaled_state.state, &context.db);

let (stack, _stack_guard) = StackRef::new(&interp.stack);
let (memory, _memory_guard) = MemoryRef::new(&interp.shared_memory);
let step = StepLog {
stack,
op: interp.current_opcode().into(),
memory,
pc: interp.program_counter() as u64,
gas_remaining: interp.gas.remaining(),
cost,
depth: context.journaled_state.depth(),
refund,
error: Some(format!("{:?}", interp.instruction_result)),
contract: self.active_call().contract.clone(),
};

let _ = self.try_fault(step, db);
}
};
}

fn log(&mut self, _interp: &mut Interpreter, _context: &mut EvmContext<DB>, _log: &Log) {}
Expand Down
144 changes: 144 additions & 0 deletions tests/it/geth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,147 @@ fn test_geth_inspector_reset() {
1000000
);
}

#[test]
fn test_geth_jstracer_op_gascost() {
/*
pragma solidity ^0.8.13;
contract Foo {
event Log(address indexed addr, uint256 value);
function foo() external {
emit Log(msg.sender, 0);
}
function bar() external {
emit Log(msg.sender, 0);
require(false, "barbarbar");
}
}
*/

let code = hex!("608060405261023e806100115f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c8063c298557814610038578063febb0f7e14610042575b5f80fd5b61004061004c565b005b61004a61009c565b005b3373ffffffffffffffffffffffffffffffffffffffff167ff950957d2407bed19dc99b718b46b4ce6090c05589006dfb86fd22c34865b23e5f6040516100929190610177565b60405180910390a2565b3373ffffffffffffffffffffffffffffffffffffffff167ff950957d2407bed19dc99b718b46b4ce6090c05589006dfb86fd22c34865b23e5f6040516100e29190610177565b60405180910390a25f61012a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610121906101ea565b60405180910390fd5b565b5f819050919050565b5f819050919050565b5f819050919050565b5f61016161015c6101578461012c565b61013e565b610135565b9050919050565b61017181610147565b82525050565b5f60208201905061018a5f830184610168565b92915050565b5f82825260208201905092915050565b7f62617262617262617200000000000000000000000000000000000000000000005f82015250565b5f6101d4600983610190565b91506101df826101a0565b602082019050919050565b5f6020820190508181035f830152610201816101c8565b905091905056fea2646970667358221220e058dc2c4bd629d62405850cc8e08e6bfad0eea187260784445dfe8f3ee0bea564736f6c634300081a0033");
let deployer = Address::ZERO;

let mut db = CacheDB::new(EmptyDB::default());

let cfg = CfgEnvWithHandlerCfg::new(CfgEnv::default(), HandlerCfg::new(SpecId::CANCUN));

let env = EnvWithHandlerCfg::new_with_cfg_env(
cfg.clone(),
BlockEnv::default(),
TxEnv {
caller: deployer,
gas_limit: 1000000,
transact_to: TransactTo::Create,
data: code.into(),
..Default::default()
},
);

let mut insp = TracingInspector::new(TracingInspectorConfig::default_geth());

// Create contract
let (res, _) = inspect(&mut db, env, &mut insp).unwrap();
let addr = match res.result {
ExecutionResult::Success { output, .. } => match output {
Output::Create(_, addr) => addr.unwrap(),
_ => panic!("Create failed"),
},
_ => panic!("Execution failed: {:?}", res.result),
};
db.commit(res.state);

let code = r#"
{
data: [],
memoryInstructions: { "MSTORE": "W", "MSTORE8": "B", "MLOAD": "R" },
fault: function (_) {},
step: function (log) {
let op = log.op.toString();
let instructions = this.memoryInstructions;
if (Object.keys(instructions).includes(op)) {
this.data.push({
op: instructions[op],
depth: log.getDepth(),
offset: log.stack.peek(0),
gasCost: log.getCost(),
memorySize: log.memory.length(),
});
}
},
result: function (ctx, _) { return { error: !!ctx.error, data: this.data }; }
}
"#;

// test with normal operation
let env = EnvWithHandlerCfg::new_with_cfg_env(
cfg.clone(),
BlockEnv::default(),
TxEnv {
caller: deployer,
gas_limit: 1000000,
transact_to: TransactTo::Call(addr),
data: hex!("c2985578").into(), // call foo
..Default::default()
},
);
let mut insp = JsInspector::new(code.to_string(), serde_json::Value::Null).unwrap();
let (res, _) = inspect(&mut db, env.clone(), &mut insp).unwrap();
println!("result: {:?}", res);
assert!(res.result.is_success());

let result = insp.json_result(res, &env, &db).unwrap();
println!("result: {}", result.to_string());

assert!(!result["error"].as_bool().unwrap());
assert_eq!(
result["data"],
serde_json::json!([
{ "op": "W", "depth": 1, "offset": "64", "gasCost": 12, "memorySize": 0 },
{ "op": "R", "depth": 1, "offset": "64", "gasCost": 3, "memorySize": 96 },
{ "op": "W", "depth": 1, "offset": "128", "gasCost": 9, "memorySize": 96 },
{ "op": "R", "depth": 1, "offset": "64", "gasCost": 3, "memorySize": 160 }
])
);

// test with reverted operation
let env = EnvWithHandlerCfg::new_with_cfg_env(
cfg,
BlockEnv::default(),
TxEnv {
caller: deployer,
gas_limit: 1000000,
transact_to: TransactTo::Call(addr),
data: "0xfebb0f7e".into(), // call bar
..Default::default()
},
);
let mut insp = JsInspector::new(code.to_string(), serde_json::Value::Null).unwrap();
let (res, _) = inspect(&mut db, env.clone(), &mut insp).unwrap();
assert!(res.result.is_success());

let result = insp.json_result(res, &env, &db).unwrap();

assert!(result["error"].as_bool().unwrap());
assert_eq!(
result["data"],
serde_json::json!([
{ "op": "W", "depth": 1, "offset": "64", "gasCost": 12, "memorySize": 0 },
{ "op": "R", "depth": 1, "offset": "64", "gasCost": 3, "memorySize": 96 },
{ "op": "W", "depth": 1, "offset": "128", "gasCost": 9, "memorySize": 96 },
{ "op": "R", "depth": 1, "offset": "64", "gasCost": 3, "memorySize": 160 },
{ "op": "W", "depth": 2, "offset": "64", "gasCost": 12, "memorySize": 0 },
{ "op": "R", "depth": 2, "offset": "64", "gasCost": 3, "memorySize": 96 },
{ "op": "W", "depth": 2, "offset": "128", "gasCost": 9, "memorySize": 96 },
{ "op": "R", "depth": 2, "offset": "64", "gasCost": 3, "memorySize": 160 },
{ "op": "R", "depth": 1, "offset": "64", "gasCost": 3, "memorySize": 160 },
{ "op": "W", "depth": 1, "offset": "128", "gasCost": 3, "memorySize": 160 },
{ "op": "W", "depth": 1, "offset": "132", "gasCost": 6, "memorySize": 160 },
{ "op": "W", "depth": 1, "offset": "164", "gasCost": 6, "memorySize": 192 },
{ "op": "W", "depth": 1, "offset": "196", "gasCost": 6, "memorySize": 224 },
{ "op": "R", "depth": 1, "offset": "64", "gasCost": 3, "memorySize": 256 }
])
);
}

0 comments on commit 2a3f6cf

Please sign in to comment.