Skip to content

Commit fe4eb45

Browse files
Soubhik-10mattsse
andauthored
feat: added JSTracer for debug_traceTransaction (#11118)
* wip * fixes * feature * clippy * clippy * refactor a bit * refactor * clippy * touchups * touchups --------- Co-authored-by: Matthias Seitz <[email protected]>
1 parent 5d0a4d8 commit fe4eb45

File tree

4 files changed

+230
-3
lines changed

4 files changed

+230
-3
lines changed

crates/anvil/core/src/eth/transaction/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,17 @@ impl PendingTransaction {
381381
}
382382
}
383383

384+
/// Converts a [`MaybeImpersonatedTransaction`] into a [`PendingTransaction`].
385+
pub fn from_maybe_impersonated(
386+
transaction: MaybeImpersonatedTransaction,
387+
) -> Result<Self, alloy_primitives::SignatureError> {
388+
if let Some(impersonated) = transaction.impersonated_sender {
389+
Ok(Self::with_impersonated(transaction.transaction, impersonated))
390+
} else {
391+
Self::new(transaction.transaction)
392+
}
393+
}
394+
384395
pub fn nonce(&self) -> u64 {
385396
self.transaction.nonce()
386397
}

crates/anvil/src/eth/backend/mem/mod.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2614,6 +2614,15 @@ impl Backend {
26142614
hash: B256,
26152615
opts: GethDebugTracingOptions,
26162616
) -> Result<GethTrace, BlockchainError> {
2617+
#[cfg(feature = "js-tracer")]
2618+
if let Some(tracer_type) = opts.tracer.as_ref()
2619+
&& tracer_type.is_js()
2620+
{
2621+
return self
2622+
.trace_tx_with_js_tracer(hash, tracer_type.as_str().to_string(), opts.clone())
2623+
.await;
2624+
}
2625+
26172626
if let Some(trace) = self.mined_geth_trace_transaction(hash, opts.clone()) {
26182627
return trace;
26192628
}
@@ -2625,6 +2634,111 @@ impl Backend {
26252634
Ok(GethTrace::Default(Default::default()))
26262635
}
26272636

2637+
/// Traces the transaction with the js tracer
2638+
#[cfg(feature = "js-tracer")]
2639+
pub async fn trace_tx_with_js_tracer(
2640+
&self,
2641+
hash: B256,
2642+
code: String,
2643+
opts: GethDebugTracingOptions,
2644+
) -> Result<GethTrace, BlockchainError> {
2645+
let GethDebugTracingOptions { tracer_config, .. } = opts;
2646+
2647+
let block = {
2648+
let storage = self.blockchain.storage.read();
2649+
let MinedTransaction { block_hash, .. } = storage
2650+
.transactions
2651+
.get(&hash)
2652+
.cloned()
2653+
.ok_or(BlockchainError::TransactionNotFound)?;
2654+
2655+
storage.blocks.get(&block_hash).cloned().ok_or(BlockchainError::BlockNotFound)?
2656+
};
2657+
2658+
let index = block
2659+
.transactions
2660+
.iter()
2661+
.position(|tx| tx.hash() == hash)
2662+
.expect("transaction not found in block");
2663+
2664+
let pool_txs: Vec<Arc<PoolTransaction>> = block.transactions[..index]
2665+
.iter()
2666+
.map(|tx| {
2667+
let pending_tx =
2668+
PendingTransaction::from_maybe_impersonated(tx.clone()).expect("is valid");
2669+
Arc::new(PoolTransaction {
2670+
pending_transaction: pending_tx,
2671+
requires: vec![],
2672+
provides: vec![],
2673+
priority: crate::eth::pool::transactions::TransactionPriority(0),
2674+
})
2675+
})
2676+
.collect();
2677+
2678+
let mut states = self.states.write();
2679+
let parent_state =
2680+
states.get(&block.header.parent_hash).ok_or(BlockchainError::BlockNotFound)?;
2681+
let mut cache_db = CacheDB::new(Box::new(parent_state));
2682+
2683+
// configure the blockenv for the block of the transaction
2684+
let mut env = self.env.read().clone();
2685+
2686+
env.evm_env.block_env = BlockEnv {
2687+
number: U256::from(block.header.number),
2688+
beneficiary: block.header.beneficiary,
2689+
timestamp: U256::from(block.header.timestamp),
2690+
difficulty: block.header.difficulty,
2691+
prevrandao: Some(block.header.mix_hash),
2692+
basefee: block.header.base_fee_per_gas.unwrap_or_default(),
2693+
gas_limit: block.header.gas_limit,
2694+
..Default::default()
2695+
};
2696+
2697+
let executor = TransactionExecutor {
2698+
db: &mut cache_db,
2699+
validator: self,
2700+
pending: pool_txs.into_iter(),
2701+
block_env: env.evm_env.block_env.clone(),
2702+
cfg_env: env.evm_env.cfg_env.clone(),
2703+
parent_hash: block.header.parent_hash,
2704+
gas_used: 0,
2705+
blob_gas_used: 0,
2706+
enable_steps_tracing: self.enable_steps_tracing,
2707+
print_logs: self.print_logs,
2708+
print_traces: self.print_traces,
2709+
call_trace_decoder: self.call_trace_decoder.clone(),
2710+
precompile_factory: self.precompile_factory.clone(),
2711+
odyssey: self.odyssey,
2712+
optimism: self.is_optimism(),
2713+
blob_params: self.blob_params(),
2714+
};
2715+
2716+
let _ = executor.execute();
2717+
2718+
let target_tx = block.transactions[index].clone();
2719+
let target_tx = PendingTransaction::from_maybe_impersonated(target_tx)?;
2720+
let tx_env = target_tx.to_revm_tx_env();
2721+
2722+
let config = tracer_config.into_json();
2723+
let mut inspector = revm_inspectors::tracing::js::JsInspector::new(code, config)
2724+
.map_err(|err| BlockchainError::Message(err.to_string()))?;
2725+
let mut evm = self.new_evm_with_inspector_ref(&cache_db, &env, &mut inspector);
2726+
2727+
let result = evm
2728+
.transact(tx_env.clone())
2729+
.map_err(|err| BlockchainError::Message(err.to_string()))?;
2730+
2731+
let trace = inspector
2732+
.json_result(
2733+
result,
2734+
&alloy_evm::IntoTxEnv::into_tx_env(tx_env),
2735+
&env.evm_env.block_env,
2736+
&cache_db,
2737+
)
2738+
.map_err(|e| BlockchainError::Message(e.to_string()))?;
2739+
Ok(GethTrace::JS(trace))
2740+
}
2741+
26282742
/// Returns code by its hash
26292743
pub async fn debug_code_by_hash(
26302744
&self,

crates/anvil/src/eth/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ pub enum BlockchainError {
7070
BlockOutOfRange(u64, u64),
7171
#[error("Resource not found")]
7272
BlockNotFound,
73+
/// Thrown when a requested transaction is not found
74+
#[error("transaction not found")]
75+
TransactionNotFound,
7376
#[error("Required data unavailable")]
7477
DataUnavailable,
7578
#[error("Trie error: {0}")]
@@ -527,6 +530,11 @@ impl<T: Serialize> ToRpcResponseResult for Result<T> {
527530
message: err.to_string().into(),
528531
data: None,
529532
},
533+
err @ BlockchainError::TransactionNotFound => RpcError {
534+
code: ErrorCode::ServerError(-32001),
535+
message: err.to_string().into(),
536+
data: None,
537+
},
530538
err @ BlockchainError::DataUnavailable => {
531539
RpcError::internal_error_with(err.to_string())
532540
}

crates/anvil/tests/it/traces.rs

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -992,11 +992,11 @@ fault: function(log) {}
992992
" Result: 1",
993993
"772: SLOAD 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:1",
994994
" Result: 1",
995-
"835: SSTORE 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:4.4498830125527143e+76 <- 1",
996-
"919: SSTORE 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:0 <- 8.008442285988055e+76",
995+
"835: SSTORE 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:44498830125527143464827115118378702402016761369235290884359940707316142178310 <- 1",
996+
"919: SSTORE 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:0 <- 80084422859880547211683076133703299733277748156566366325829078699459944778998",
997997
"712: SLOAD 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:0",
998998
" Result: 0",
999-
"765: SSTORE 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:5.465844868464591e+47 <- 0",
999+
"765: SSTORE 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:546584486846459126461364135121053344201067465379 <- 0",
10001000
];
10011001

10021002
let actual: Vec<String> = result
@@ -1008,3 +1008,97 @@ fault: function(log) {}
10081008

10091009
assert_eq!(actual, expected);
10101010
}
1011+
1012+
#[cfg(feature = "js-tracer")]
1013+
#[tokio::test(flavor = "multi_thread")]
1014+
async fn test_debug_trace_transaction_js_tracer() {
1015+
let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Prague.into()));
1016+
let (api, handle) = spawn(node_config).await;
1017+
let provider = crate::utils::http_provider(&handle.http_endpoint());
1018+
1019+
let wallets = handle.dev_wallets().collect::<Vec<_>>();
1020+
let from = wallets[0].address();
1021+
api.anvil_add_balance(from, U256::MAX).await.unwrap();
1022+
api.anvil_add_balance(wallets[1].address(), U256::MAX).await.unwrap();
1023+
1024+
let multicall_contract = Multicall::deploy(&provider).await.unwrap();
1025+
let simple_storage_contract =
1026+
SimpleStorage::deploy(&provider, "init value".to_string()).await.unwrap();
1027+
1028+
let set_value = simple_storage_contract.setValue("bar".to_string());
1029+
let set_value_calldata = set_value.calldata();
1030+
1031+
let internal_call_tx_builder = multicall_contract.aggregate(vec![Multicall::Call {
1032+
target: *simple_storage_contract.address(),
1033+
callData: set_value_calldata.to_owned(),
1034+
}]);
1035+
1036+
let internal_call_tx_calldata = internal_call_tx_builder.calldata().to_owned();
1037+
1038+
let internal_call_tx = TransactionRequest::default()
1039+
.from(wallets[1].address())
1040+
.to(*multicall_contract.address())
1041+
.with_input(internal_call_tx_calldata)
1042+
.with_gas_limit(1_000_000)
1043+
.with_max_fee_per_gas(100_000_000_000)
1044+
.with_max_priority_fee_per_gas(100_000_000_000);
1045+
1046+
let receipt = provider
1047+
.send_transaction(internal_call_tx.into())
1048+
.await
1049+
.unwrap()
1050+
.get_receipt()
1051+
.await
1052+
.unwrap();
1053+
let js_tracer_code = r#"
1054+
{
1055+
data: [],
1056+
step: function(log) {
1057+
var op = log.op.toString();
1058+
var pc = log.getPC();
1059+
var addr = log.contract.getAddress();
1060+
1061+
if (op === "SLOAD") {
1062+
this.data.push(pc + ": SLOAD " + addr + ":" + log.stack.peek(0));
1063+
this.data.push(" Result: " + log.stack.peek(0));
1064+
} else if (op === "SSTORE") {
1065+
this.data.push(pc + ": SSTORE " + addr + ":" + log.stack.peek(1) + " <- " + log.stack.peek(0));
1066+
}
1067+
},
1068+
result: function() {
1069+
return this.data;
1070+
},
1071+
fault: function(log) {}
1072+
}
1073+
"#;
1074+
1075+
let expected = vec![
1076+
"547: SLOAD 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:0",
1077+
" Result: 0",
1078+
"1907: SLOAD 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:1",
1079+
" Result: 1",
1080+
"772: SLOAD 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:1",
1081+
" Result: 1",
1082+
"835: SSTORE 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:44498830125527143464827115118378702402016761369235290884359940707316142178310 <- 1",
1083+
"919: SSTORE 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:0 <- 80084422859880547211683076133703299733277748156566366325829078699459944778998",
1084+
"712: SLOAD 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:0",
1085+
" Result: 0",
1086+
"765: SSTORE 231,241,114,94,119,52,206,40,143,131,103,225,187,20,62,144,187,63,5,18:546584486846459126461364135121053344201067465379 <- 0",
1087+
];
1088+
let result = api
1089+
.debug_trace_transaction(
1090+
receipt.transaction_hash,
1091+
GethDebugTracingOptions::js_tracer(js_tracer_code),
1092+
)
1093+
.await
1094+
.unwrap();
1095+
1096+
let actual: Vec<String> = result
1097+
.try_into_json_value()
1098+
.ok()
1099+
.and_then(|val| val.as_array().cloned())
1100+
.map(|arr| arr.into_iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
1101+
.unwrap_or_default();
1102+
1103+
assert_eq!(actual, expected);
1104+
}

0 commit comments

Comments
 (0)