From b275a449ea1e9e2706e4ba65801f6ec789aa940e Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Tue, 22 Jul 2025 10:21:58 +0200 Subject: [PATCH 1/4] Etherlink/Revm: enable precompile tracing with call tracer The commit also contain a small cleanup on helper file. No reason to put it here, but doing a seperate MR/commit seems overkill. --- .../kernel_latest/revm/src/helpers/rlp.rs | 6 +- .../revm/src/inspectors/call_tracer.rs | 55 ++++++++++++++++++- etherlink/kernel_latest/revm/src/lib.rs | 9 ++- .../revm/src/precompile_provider.rs | 5 +- 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/etherlink/kernel_latest/revm/src/helpers/rlp.rs b/etherlink/kernel_latest/revm/src/helpers/rlp.rs index d86f16f84d8b..b88ae9d50a3d 100644 --- a/etherlink/kernel_latest/revm/src/helpers/rlp.rs +++ b/etherlink/kernel_latest/revm/src/helpers/rlp.rs @@ -21,7 +21,7 @@ pub fn append_address<'a>( } pub fn append_option_canonical<'a, T, Enc>( - stream: &'a mut rlp::RlpStream, + stream: &'a mut RlpStream, v: &Option, append: Enc, ) where @@ -38,11 +38,11 @@ pub fn append_option_canonical<'a, T, Enc>( } } -pub fn append_option_u64_le(stream: &mut rlp::RlpStream, v: &Option) { +pub fn append_option_u64_le(stream: &mut RlpStream, v: &Option) { append_option_canonical(stream, v, append_u64_le) } -pub fn append_option_address(stream: &mut rlp::RlpStream, address: &Option
) { +pub fn append_option_address(stream: &mut RlpStream, address: &Option
) { append_option_canonical(stream, address, append_address) } diff --git a/etherlink/kernel_latest/revm/src/inspectors/call_tracer.rs b/etherlink/kernel_latest/revm/src/inspectors/call_tracer.rs index c56601ab04c0..052ed002fee7 100644 --- a/etherlink/kernel_latest/revm/src/inspectors/call_tracer.rs +++ b/etherlink/kernel_latest/revm/src/inspectors/call_tracer.rs @@ -9,20 +9,23 @@ use crate::{ append_address, append_option_address, append_option_canonical, append_option_u64_le, append_u16_le, append_u256_le, append_u64_le, }, + precompile_provider::EtherlinkPrecompiles, }; -use tezos_ethereum::Log as RlpLog; use revm::{ context::{ContextTr, CreateScheme, JournalTr, Transaction}, interpreter::{ gas::calculate_initial_tx_gas_for_tx, interpreter::ReturnDataImpl, interpreter_types::StackTr, CallInputs, CallOutcome, CallScheme, CreateInputs, - CreateOutcome, InitialAndFloorGas, InstructionResult, InterpreterTypes, + CreateOutcome, Gas, InitialAndFloorGas, InstructionResult, InterpreterResult, + InterpreterTypes, }, primitives::{hardfork::SpecId, hash_map::HashMap, Address, Bytes, Log, B256, U256}, Inspector, }; use rlp::{Encodable, RlpStream}; +use std::ops::Range; +use tezos_ethereum::Log as RlpLog; use tezos_evm_logging::{log, Level::Debug}; use tezos_evm_runtime::runtime::Runtime; @@ -170,6 +173,7 @@ impl CallTrace { pub struct CallTracer { config: CallTracerConfig, + precompiles: EtherlinkPrecompiles, call_trace: HashMap, transaction_hash: Option, initial_gas: u64, @@ -179,11 +183,13 @@ pub struct CallTracer { impl CallTracer { pub fn new( config: CallTracerConfig, + precompiles: EtherlinkPrecompiles, spec_id: SpecId, transaction_hash: Option, ) -> Self { Self { config, + precompiles, call_trace: HashMap::with_capacity(1), transaction_hash, initial_gas: 0, @@ -266,11 +272,13 @@ where CallScheme::CallCode => ("CALLCODE", inputs.target_address), }; + let call_data = inputs.input.bytes(context); + let mut call_trace = CallTrace::new_minimal_trace( type_.into(), from, inputs.value.get(), - inputs.input.bytes(context).to_vec(), + call_data.to_vec(), depth, ); @@ -279,6 +287,47 @@ where self.set_call_trace(depth, call_trace); + if let Some(precompile) = self + .precompiles + .builtins + .precompiles + .get(&inputs.bytecode_address) + { + // Hack-ish behavior. In case the invoked address is a precompile we need to + // pre-simulate its result because the `call_end` hook is never called when a + // precompile contract is called. + + let memory_offset = Range { start: 0, end: 0 }; // Ignored. + let mut outcome = match precompile(&call_data, inputs.gas_limit) { + Ok(result) => CallOutcome { + result: InterpreterResult { + result: InstructionResult::Return, + output: result.bytes, + gas: Gas::new_spent(result.gas_used), + }, + memory_offset, + }, + Err(_) => CallOutcome { + result: InterpreterResult { + result: InstructionResult::PrecompileError, + // No return data, indicates a precompile contract error. + output: Bytes::new(), + gas: Gas::new_spent(inputs.gas_limit), + }, + memory_offset, + }, + }; + + >::call_end( + self, + context, + inputs, + &mut outcome, + ); + + return None; + } + // NB: Always return [None] or else the result of the call will be overriden. None } diff --git a/etherlink/kernel_latest/revm/src/lib.rs b/etherlink/kernel_latest/revm/src/lib.rs index 4067f33b5cfc..5874dcde02d6 100644 --- a/etherlink/kernel_latest/revm/src/lib.rs +++ b/etherlink/kernel_latest/revm/src/lib.rs @@ -143,13 +143,18 @@ fn tx_env<'a, Host: Runtime>( Ok(tx_env) } -fn get_inspector_from(tracer_input: TracerInput, spec_id: SpecId) -> EtherlinkInspector { +fn get_inspector_from( + tracer_input: TracerInput, + precompiles: EtherlinkPrecompiles, + spec_id: SpecId, +) -> EtherlinkInspector { match tracer_input { TracerInput::CallTracer(CallTracerInput { config, transaction_hash, }) => EtherlinkInspector::CallTracer(Box::new(CallTracer::new( config, + precompiles, spec_id, transaction_hash, ))), @@ -257,7 +262,7 @@ pub fn run_transaction<'a, Host: Runtime>( ); if let Some(tracer_input) = tracer_input { - let inspector = get_inspector_from(tracer_input, spec_id); + let inspector = get_inspector_from(tracer_input, precompiles.clone(), spec_id); let mut evm = evm_inspect( db, diff --git a/etherlink/kernel_latest/revm/src/precompile_provider.rs b/etherlink/kernel_latest/revm/src/precompile_provider.rs index e9b1ac784601..afa97453f8c8 100644 --- a/etherlink/kernel_latest/revm/src/precompile_provider.rs +++ b/etherlink/kernel_latest/revm/src/precompile_provider.rs @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2025 Nomadic Labs +// SPDX-FileCopyrightText: 2025 Functori // // SPDX-License-Identifier: MIT @@ -18,9 +19,9 @@ use crate::{ table::table_precompile, }; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct EtherlinkPrecompiles { - builtins: EthPrecompiles, + pub builtins: EthPrecompiles, } impl EtherlinkPrecompiles { -- GitLab From ea2bc996d7465e335a1793bcb470330231a034fd Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Tue, 22 Jul 2025 10:04:15 +0200 Subject: [PATCH 2/4] Etherlink/Tezt: enable [test_trace_transaction_calltracer_precompiles] with revm --- etherlink/tezt/tests/evm_sequencer.ml | 1 + 1 file changed, 1 insertion(+) diff --git a/etherlink/tezt/tests/evm_sequencer.ml b/etherlink/tezt/tests/evm_sequencer.ml index 810e2b63ea7a..9de850a9d15d 100644 --- a/etherlink/tezt/tests/evm_sequencer.ml +++ b/etherlink/tezt/tests/evm_sequencer.ml @@ -8610,6 +8610,7 @@ let test_trace_transaction_calltracer_precompiles = ~da_fee:Wei.zero ~maximum_allowed_ticks:100_000_000_000_000L ~time_between_blocks:Nothing + ~use_revm:activate_revm_registration @@ fun {sequencer; evm_version; _} _protocol -> let endpoint = Evm_node.endpoint sequencer in let sender = Eth_account.bootstrap_accounts.(0) in -- GitLab From 9415b9680ce544f5d1163457d2e216da285ab8f2 Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Tue, 22 Jul 2025 16:26:56 +0200 Subject: [PATCH 3/4] Etherlink/Revm: plain tracer implementation --- .../revm/src/inspectors/call_tracer.rs | 17 ++-- .../kernel_latest/revm/src/inspectors/mod.rs | 1 + .../revm/src/inspectors/plain.rs | 80 +++++++++++++++++++ etherlink/kernel_latest/revm/src/lib.rs | 20 ++++- 4 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 etherlink/kernel_latest/revm/src/inspectors/plain.rs diff --git a/etherlink/kernel_latest/revm/src/inspectors/call_tracer.rs b/etherlink/kernel_latest/revm/src/inspectors/call_tracer.rs index 052ed002fee7..507b1dcc6b05 100644 --- a/etherlink/kernel_latest/revm/src/inspectors/call_tracer.rs +++ b/etherlink/kernel_latest/revm/src/inspectors/call_tracer.rs @@ -116,23 +116,23 @@ impl CallTrace { } } - fn add_to(&mut self, to: Option
) { + pub fn add_to(&mut self, to: Option
) { self.to = to; } - fn add_gas(&mut self, gas: Option) { + pub fn add_gas(&mut self, gas: Option) { self.gas = gas; } - fn add_gas_used(&mut self, gas_used: u64) { + pub fn add_gas_used(&mut self, gas_used: u64) { self.gas_used = gas_used; } - fn add_output(&mut self, output: Option>) { + pub fn add_output(&mut self, output: Option>) { self.output = output; } - fn add_error(&mut self, error: Option>) { + pub fn add_error(&mut self, error: Option>) { self.error = error; } @@ -162,7 +162,7 @@ impl CallTrace { self.logs = logs; } - fn store(&self, host: &mut impl Runtime, transaction_hash: &Option) { + pub fn store(&self, host: &mut impl Runtime, transaction_hash: &Option) { store_call_trace(host, self, transaction_hash) .inspect_err(|err| { log!(host, Debug, "Storing call trace failed with: {err:?}") @@ -197,6 +197,11 @@ impl CallTracer { } } + #[inline] + pub fn tx_hash(&self) -> Option { + self.transaction_hash + } + #[inline] fn set_call_trace(&mut self, depth: u16, call_trace: CallTrace) { self.call_trace.insert(depth, call_trace); diff --git a/etherlink/kernel_latest/revm/src/inspectors/mod.rs b/etherlink/kernel_latest/revm/src/inspectors/mod.rs index 19450b4c6073..7b1f7c075bbd 100644 --- a/etherlink/kernel_latest/revm/src/inspectors/mod.rs +++ b/etherlink/kernel_latest/revm/src/inspectors/mod.rs @@ -32,6 +32,7 @@ use tezos_evm_runtime::runtime::Runtime; pub mod call_tracer; pub mod noop; +pub mod plain; mod storage; diff --git a/etherlink/kernel_latest/revm/src/inspectors/plain.rs b/etherlink/kernel_latest/revm/src/inspectors/plain.rs new file mode 100644 index 000000000000..4b91a542fe21 --- /dev/null +++ b/etherlink/kernel_latest/revm/src/inspectors/plain.rs @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2025 Functori +// +// SPDX-License-Identifier: MIT + +// Every tracers rely on the inspector interface which is triggered when there is actual +// EVM execution. For plain inspector hooks will not be activated at any point. +// This module provides an extensible interface so that a plain transfer/blank call can be +// stored as a proper traced transaction. +// The implemention is ad-hoc for each tracer on purpose to match the default cases expected +// for tooling compatibility. + +use crate::world_state_handler::StorageAccount; + +use super::{call_tracer::CallTrace, EtherlinkInspector}; +use revm::{ + context::result::ExecutionResult, + primitives::{Address, U256}, +}; +use tezos_evm_logging::{log, Level::Debug}; +use tezos_evm_runtime::runtime::Runtime; + +pub fn is_plain_transaction( + host: &mut impl Runtime, + destination: &Option
, +) -> bool { + if let Some(destination) = destination { + match StorageAccount::from_address(destination) { + Ok(storage_account) => match storage_account.code(host) { + Ok(None) => true, + Ok(Some(bytecode)) => bytecode.is_empty(), + _ => false, + }, + Err(err) => { + // Unexpected case, since we're at tracing level we can't do much since there's + // no point in having errors. We just log what happened: + log!( + host, + Debug, + "Unexpected error while checking if tx is a plain one: {err:?}" + ); + false + } + } + } else { + // It's a contract creation. + false + } +} + +pub fn minimal_plain_trace( + host: &mut impl Runtime, + inspector: EtherlinkInspector, + from: Address, + to: Option
, + value: U256, + gas_limit: u64, + result: &ExecutionResult, +) { + match inspector { + EtherlinkInspector::NoOp(_) => (), + EtherlinkInspector::CallTracer(call_tracer) => { + let mut call_trace = + CallTrace::new_minimal_trace("CALL".into(), from, value, vec![], 0); + call_trace.add_to(to); + call_trace.add_gas(Some(gas_limit)); + call_trace.add_gas_used(21_000); + match result { + ExecutionResult::Success { .. } => (), + ExecutionResult::Revert { output, .. } => { + call_trace.add_error(Some("Reverted".into())); + call_trace.add_output(Some(output.to_vec())); + } + ExecutionResult::Halt { reason, .. } => { + call_trace.add_error(Some(format!("{reason:?}").into())); + } + } + call_trace.store(host, &call_tracer.tx_hash()); + } + } +} diff --git a/etherlink/kernel_latest/revm/src/lib.rs b/etherlink/kernel_latest/revm/src/lib.rs index 5874dcde02d6..5565e7aa10a4 100644 --- a/etherlink/kernel_latest/revm/src/lib.rs +++ b/etherlink/kernel_latest/revm/src/lib.rs @@ -7,8 +7,10 @@ use crate::{database::PrecompileDatabase, send_outbox_message::Withdrawal}; use database::EtherlinkVMDB; use helpers::storage::u256_to_le_bytes; use inspectors::{ - call_tracer::CallTracer, noop::NoInspector, CallTracerInput, EtherlinkInspector, - EvmInspection, TracerInput, + call_tracer::CallTracer, + noop::NoInspector, + plain::{is_plain_transaction, minimal_plain_trace}, + CallTracerInput, EtherlinkInspector, EvmInspection, TracerInput, }; use precompile_provider::EtherlinkPrecompiles; use revm::{ @@ -278,6 +280,20 @@ pub fn run_transaction<'a, Host: Runtime>( let withdrawals = evm.db_mut().take_withdrawals(); + if is_plain_transaction(evm.ctx.db_mut().host, &destination) { + // Nothing was stored during inspection as no bytecode was executed, we + // need to store the minimal plain trace w.r.t. the given inspector. + minimal_plain_trace( + evm.ctx.db_mut().host, + evm.inspector, + caller, + destination, + value, + gas_limit, + &result, + ); + } + Ok(ExecutionOutcome { result, withdrawals, -- GitLab From d1393e859763d3284ae27e36cfd7256186840032 Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Tue, 22 Jul 2025 15:50:57 +0200 Subject: [PATCH 4/4] Etherlink/Tezt: enable [test_trace_transaction_calltracer_on_simple_transfer] with revm --- etherlink/tezt/tests/evm_sequencer.ml | 1 + 1 file changed, 1 insertion(+) diff --git a/etherlink/tezt/tests/evm_sequencer.ml b/etherlink/tezt/tests/evm_sequencer.ml index 9de850a9d15d..dc6366c16d84 100644 --- a/etherlink/tezt/tests/evm_sequencer.ml +++ b/etherlink/tezt/tests/evm_sequencer.ml @@ -8578,6 +8578,7 @@ let test_trace_transaction_calltracer_on_simple_transfer = ~title:"debug_traceTransaction can trace a simple transfer" ~da_fee:Wei.zero ~time_between_blocks:Nothing + ~use_revm:activate_revm_registration @@ fun {sequencer; _} _protocol -> let endpoint = Evm_node.endpoint sequencer in let sender = Eth_account.bootstrap_accounts.(0) in -- GitLab