feat(rpc): kickoff geth traces (#1772)

This commit is contained in:
Matthias Seitz
2023-03-19 19:29:22 +01:00
committed by GitHub
parent 0128d42b4b
commit 0936523e3d
12 changed files with 249 additions and 42 deletions

View File

@ -1,6 +1,6 @@
//! Geth trace builder
use crate::tracing::{types::CallTraceNode, TraceInspectorConfig};
use crate::tracing::{types::CallTraceNode, TracingInspectorConfig};
use reth_primitives::{Address, JsonU256, H256, U256};
use reth_rpc_types::trace::geth::*;
use revm::interpreter::opcode;
@ -12,12 +12,12 @@ pub struct GethTraceBuilder {
/// Recorded trace nodes.
nodes: Vec<CallTraceNode>,
/// How the traces were recorded
_config: TraceInspectorConfig,
_config: TracingInspectorConfig,
}
impl GethTraceBuilder {
/// Returns a new instance of the builder
pub(crate) fn new(nodes: Vec<CallTraceNode>, _config: TraceInspectorConfig) -> Self {
pub(crate) fn new(nodes: Vec<CallTraceNode>, _config: TracingInspectorConfig) -> Self {
Self { nodes, _config }
}
@ -29,7 +29,7 @@ impl GethTraceBuilder {
storage: &mut HashMap<Address, BTreeMap<H256, H256>>,
trace_node: &CallTraceNode,
struct_logs: &mut Vec<StructLog>,
opts: &GethDebugTracingOptions,
opts: &GethDefaultTracingOptions,
) {
let mut child_id = 0;
// Iterate over the steps inside the given trace
@ -80,7 +80,7 @@ impl GethTraceBuilder {
&self,
// TODO(mattsse): This should be the total gas used, or gas used by last CallTrace?
receipt_gas_used: U256,
opts: GethDebugTracingOptions,
opts: GethDefaultTracingOptions,
) -> DefaultFrame {
if self.nodes.is_empty() {
return Default::default()

View File

@ -1,4 +1,4 @@
use crate::tracing::{types::CallTraceNode, TraceInspectorConfig};
use crate::tracing::{types::CallTraceNode, TracingInspectorConfig};
use reth_rpc_types::{trace::parity::*, TransactionInfo};
/// A type for creating parity style traces
@ -7,12 +7,12 @@ pub struct ParityTraceBuilder {
/// Recorded trace nodes
nodes: Vec<CallTraceNode>,
/// How the traces were recorded
_config: TraceInspectorConfig,
_config: TracingInspectorConfig,
}
impl ParityTraceBuilder {
/// Returns a new instance of the builder
pub(crate) fn new(nodes: Vec<CallTraceNode>, _config: TraceInspectorConfig) -> Self {
pub(crate) fn new(nodes: Vec<CallTraceNode>, _config: TracingInspectorConfig) -> Self {
Self { nodes, _config }
}

View File

@ -1,9 +1,9 @@
/// Gives guidance to the [TracingInspector](crate::tracing::TracingInspector).
///
/// Use [TraceInspectorConfig::default_parity] or [TraceInspectorConfig::default_geth] to get the
/// default configs for specific styles of traces.
/// Use [TracingInspectorConfig::default_parity] or [TracingInspectorConfig::default_geth] to get
/// the default configs for specific styles of traces.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct TraceInspectorConfig {
pub struct TracingInspectorConfig {
/// Whether to record every individual opcode level step.
pub record_steps: bool,
/// Whether to record individual memory snapshots.
@ -14,7 +14,7 @@ pub struct TraceInspectorConfig {
pub record_state_diff: bool,
}
impl TraceInspectorConfig {
impl TracingInspectorConfig {
/// Returns a config with everything enabled.
pub const fn all() -> Self {
Self {

View File

@ -23,7 +23,7 @@ mod config;
mod types;
mod utils;
pub use builder::{geth::GethTraceBuilder, parity::ParityTraceBuilder};
pub use config::TraceInspectorConfig;
pub use config::TracingInspectorConfig;
/// An inspector that collects call traces.
///
@ -36,7 +36,7 @@ pub use config::TraceInspectorConfig;
#[derive(Debug, Clone)]
pub struct TracingInspector {
/// Configures what and how the inspector records traces.
config: TraceInspectorConfig,
config: TracingInspectorConfig,
/// Records all call traces
traces: CallTraceArena,
trace_stack: Vec<usize>,
@ -52,7 +52,7 @@ pub struct TracingInspector {
impl TracingInspector {
/// Returns a new instance for the given config
pub fn new(config: TraceInspectorConfig) -> Self {
pub fn new(config: TracingInspectorConfig) -> Self {
Self {
config,
traces: Default::default(),

View File

@ -39,6 +39,8 @@ pub struct CallLogFrame {
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallConfig {
/// When set to true, this will only trace the primary (top-level) call and not any sub-calls.
/// It eliminates the additional processing for each call frame
#[serde(default, skip_serializing_if = "Option::is_none")]
pub only_top_call: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -60,7 +62,7 @@ mod tests {
#[test]
fn test_serialize_call_trace() {
let mut opts = GethDebugTracingCallOptions::default();
opts.tracing_options.disable_storage = Some(false);
opts.tracing_options.config.disable_storage = Some(false);
opts.tracing_options.tracer =
Some(GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::CallTracer));
opts.tracing_options.tracer_config =

View File

@ -134,12 +134,28 @@ impl From<serde_json::Value> for GethTrace {
/// See <https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers>
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub enum GethDebugBuiltInTracerType {
/// The 4byteTracer collects the function selectors of every function executed in the lifetime
/// of a transaction, along with the size of the supplied call data. The result is a
/// [FourByteFrame] where the keys are SELECTOR-CALLDATASIZE and the values are number of
/// occurrences of this key.
#[serde(rename = "4byteTracer")]
FourByteTracer,
/// The callTracer tracks all the call frames executed during a transaction, including depth 0.
/// The result will be a nested list of call frames, resembling how EVM works. They form a tree
/// with the top-level call at root and sub-calls as children of the higher levels.
#[serde(rename = "callTracer")]
CallTracer,
/// The prestate tracer has two modes: prestate and diff. The prestate mode returns the
/// accounts necessary to execute a given transaction. diff mode returns the differences
/// between the transaction's pre and post-state (i.e. what changed because the transaction
/// happened). The prestateTracer defaults to prestate mode. It reexecutes the given
/// transaction and tracks every part of state that is touched. This is similar to the concept
/// of a stateless witness, the difference being this tracer doesn't return any cryptographic
/// proof, rather only the trie leaves. The result is an object. The keys are addresses of
/// accounts.
#[serde(rename = "prestateTracer")]
PreStateTracer,
/// This tracer is noop. It returns an empty object and is only meant for testing the setup.
#[serde(rename = "noopTracer")]
NoopTracer,
}
@ -152,6 +168,22 @@ pub enum GethDebugBuiltInTracerConfig {
PreStateTracer(PreStateConfig),
}
// === impl GethDebugBuiltInTracerConfig ===
impl GethDebugBuiltInTracerConfig {
/// Returns true if the config matches the given tracer
pub fn matches_tracer(&self, tracer: &GethDebugBuiltInTracerType) -> bool {
matches!(
(self, tracer),
(GethDebugBuiltInTracerConfig::CallTracer(_), GethDebugBuiltInTracerType::CallTracer,) |
(
GethDebugBuiltInTracerConfig::PreStateTracer(_),
GethDebugBuiltInTracerType::PreStateTracer,
)
)
}
}
/// Available tracers
///
/// See <https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers> and <https://geth.ethereum.org/docs/developers/evm-tracing/custom-tracer>
@ -174,30 +206,97 @@ pub enum GethDebugTracerConfig {
JsTracer(serde_json::Value),
}
// === impl GethDebugTracerConfig ===
impl GethDebugTracerConfig {
/// Returns the [CallConfig] if it is a call config.
pub fn into_call_config(self) -> Option<CallConfig> {
match self {
GethDebugTracerConfig::BuiltInTracer(GethDebugBuiltInTracerConfig::CallTracer(cfg)) => {
Some(cfg)
}
_ => None,
}
}
/// Returns the [PreStateConfig] if it is a call config.
pub fn into_pre_state_config(self) -> Option<PreStateConfig> {
match self {
GethDebugTracerConfig::BuiltInTracer(GethDebugBuiltInTracerConfig::PreStateTracer(
cfg,
)) => Some(cfg),
_ => None,
}
}
/// Returns true if the config matches the given tracer
pub fn matches_tracer(&self, tracer: &GethDebugTracerType) -> bool {
match (self, tracer) {
(_, GethDebugTracerType::BuiltInTracer(tracer)) => self.matches_builtin_tracer(tracer),
(GethDebugTracerConfig::JsTracer(_), GethDebugTracerType::JsTracer(_)) => true,
_ => false,
}
}
/// Returns true if the config matches the given tracer
pub fn matches_builtin_tracer(&self, tracer: &GethDebugBuiltInTracerType) -> bool {
match (self, tracer) {
(GethDebugTracerConfig::BuiltInTracer(config), tracer) => config.matches_tracer(tracer),
(GethDebugTracerConfig::JsTracer(_), _) => false,
}
}
}
/// Bindings for additional `debug_traceTransaction` options
///
/// See <https://geth.ethereum.org/docs/rpc/ns-debug#debug_tracetransaction>
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct GethDebugTracingOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_storage: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_stack: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_memory: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_return_data: Option<bool>,
#[serde(default, flatten)]
pub config: GethDefaultTracingOptions,
/// The custom tracer to use.
///
/// If `None` then the default structlog tracer is used.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tracer: Option<GethDebugTracerType>,
/// tracerConfig is slated for Geth v1.11.0
/// See <https://github.com/ethereum/go-ethereum/issues/26513>
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tracer_config: Option<GethDebugTracerConfig>,
/// A string of decimal integers that overrides the JavaScript-based tracing calls default
/// timeout of 5 seconds.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<String>,
}
/// Default tracing options for the struct looger
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct GethDefaultTracingOptions {
/// enable memory capture
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_memory: Option<bool>,
/// disable stack capture
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_stack: Option<bool>,
/// disable storage capture
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_storage: Option<bool>,
/// enable return data capture
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_return_data: Option<bool>,
/// print output during capture end
#[serde(default, skip_serializing_if = "Option::is_none")]
pub debug: Option<bool>,
/// maximum length of output, but zero means unlimited
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u64>,
/// Chain overrides, can be used to execute a trace using future fork rules
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overrides: Option<serde_json::Value>,
}
/// Bindings for additional `debug_traceCall` options
///
/// See <https://geth.ethereum.org/docs/rpc/ns-debug#debug_tracecall>

View File

@ -59,7 +59,7 @@ mod tests {
#[test]
fn test_serialize_pre_state_trace() {
let mut opts = GethDebugTracingCallOptions::default();
opts.tracing_options.disable_storage = Some(false);
opts.tracing_options.config.disable_storage = Some(false);
opts.tracing_options.tracer =
Some(GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::PreStateTracer));
opts.tracing_options.tracer_config = Some(GethDebugTracerConfig::BuiltInTracer(

View File

@ -1,12 +1,29 @@
use crate::{result::internal_rpc_err, EthApiSpec};
use crate::{
eth::{
error::{EthApiError, EthResult},
revm_utils::inspect,
EthTransactions,
},
result::internal_rpc_err,
EthApiSpec,
};
use async_trait::async_trait;
use jsonrpsee::core::RpcResult;
use reth_primitives::{BlockId, BlockNumberOrTag, Bytes, H256};
use reth_primitives::{BlockId, BlockNumberOrTag, Bytes, H256, U256};
use reth_revm::{
database::{State, SubState},
env::tx_env_with_recovered,
tracing::{TracingInspector, TracingInspectorConfig},
};
use reth_rpc_api::DebugApiServer;
use reth_rpc_types::{
trace::geth::{BlockTraceResult, GethDebugTracingOptions, GethTraceFrame, TraceResult},
trace::geth::{
BlockTraceResult, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingOptions,
GethTraceFrame, NoopFrame, TraceResult,
},
CallRequest, RichBlock,
};
use revm::primitives::Env;
/// `debug` API implementation.
///
@ -14,7 +31,7 @@ use reth_rpc_types::{
#[non_exhaustive]
pub struct DebugApi<Eth> {
/// The implementation of `eth` API
eth: Eth,
eth_api: Eth,
}
// === impl DebugApi ===
@ -22,7 +39,81 @@ pub struct DebugApi<Eth> {
impl<Eth> DebugApi<Eth> {
/// Create a new instance of the [DebugApi]
pub fn new(eth: Eth) -> Self {
Self { eth }
Self { eth_api: eth }
}
}
// === impl DebugApi ===
impl<Eth> DebugApi<Eth>
where
Eth: EthTransactions + 'static,
{
/// Trace the transaction according to the provided options.
///
/// Ref: <https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers>
pub async fn debug_trace_transaction(
&self,
tx_hash: H256,
opts: GethDebugTracingOptions,
) -> EthResult<GethTraceFrame> {
let (transaction, at) = match self.eth_api.transaction_by_hash_at(tx_hash).await? {
None => return Err(EthApiError::TransactionNotFound),
Some(res) => res,
};
let (cfg, block, at) = self.eth_api.evm_env_at(at).await?;
let tx = transaction.into_recovered();
self.eth_api.with_state_at(at, |state| {
let tx = tx_env_with_recovered(&tx);
let env = Env { cfg, block, tx };
let db = SubState::new(State::new(state));
let GethDebugTracingOptions { config, tracer, tracer_config, .. } = opts;
if let Some(tracer) = tracer {
// valid matching config
if let Some(ref config) = tracer_config {
if !config.matches_tracer(&tracer) {
return Err(EthApiError::InvalidTracerConfig)
}
}
return match tracer {
GethDebugTracerType::BuiltInTracer(tracer) => match tracer {
GethDebugBuiltInTracerType::FourByteTracer => {
todo!()
}
GethDebugBuiltInTracerType::CallTracer => {
todo!()
}
GethDebugBuiltInTracerType::PreStateTracer => {
todo!()
}
GethDebugBuiltInTracerType::NoopTracer => Ok(NoopFrame::default().into()),
},
GethDebugTracerType::JsTracer(_) => {
Err(EthApiError::Unsupported("javascript tracers are unsupported."))
}
}
}
// default structlog tracer
let inspector_config = TracingInspectorConfig::default_geth()
.set_memory_snapshots(config.enable_memory.unwrap_or_default())
.set_stack_snapshots(!config.disable_stack.unwrap_or_default())
.set_state_diffs(!config.disable_storage.unwrap_or_default());
let mut inspector = TracingInspector::new(inspector_config);
let (res, _) = inspect(db, env, &mut inspector)?;
let gas_used = res.result.gas_used();
let frame = inspector.into_geth_builder().geth_traces(U256::from(gas_used), config);
Ok(frame.into())
})
}
}
@ -96,10 +187,10 @@ where
/// Handler for `debug_traceTransaction`
async fn debug_trace_transaction(
&self,
_tx_hash: H256,
_opts: GethDebugTracingOptions,
tx_hash: H256,
opts: GethDebugTracingOptions,
) -> RpcResult<GethTraceFrame> {
Err(internal_rpc_err("unimplemented"))
Ok(DebugApi::debug_trace_transaction(self, tx_hash, opts).await?)
}
/// Handler for `debug_traceCall`

View File

@ -446,7 +446,7 @@ mod tests {
let hash = H256::random();
let gas_limit: u64 = random();
let gas_used: u64 = random();
let base_fee_per_gas: Option<u64> = random::<bool>().then(|| random());
let base_fee_per_gas: Option<u64> = random::<bool>().then(random);
let header = Header {
number: newest_block - i,

View File

@ -1,7 +1,7 @@
//! Implementation specific Errors for the `eth_` namespace.
use crate::result::{internal_rpc_err, rpc_err};
use jsonrpsee::{core::Error as RpcError, types::error::INVALID_PARAMS_CODE};
use crate::result::{internal_rpc_err, invalid_params_rpc_err, rpc_err};
use jsonrpsee::core::Error as RpcError;
use reth_primitives::{constants::SELECTOR_LEN, Address, Bytes, U256};
use reth_rpc_types::{error::EthRpcErrorCode, BlockError};
use reth_transaction_pool::error::{InvalidPoolTransactionError, PoolError};
@ -55,6 +55,15 @@ pub enum EthApiError {
/// Error related to signing
#[error(transparent)]
Signing(#[from] SignError),
/// Thrown when a transaction was requested but not matching transaction exists
#[error("transaction not found")]
TransactionNotFound,
/// Some feature is unsupported
#[error("unsupported")]
Unsupported(&'static str),
/// When tracer config does not match the tracer
#[error("invalid tracer config")]
InvalidTracerConfig,
}
impl From<EthApiError> for RpcError {
@ -69,14 +78,15 @@ impl From<EthApiError> for RpcError {
EthApiError::ConflictingRequestGasPriceAndTipSet { .. } |
EthApiError::RequestLegacyGasPriceAndTipSet { .. } |
EthApiError::Signing(_) |
EthApiError::BothStateAndStateDiffInOverride(_) => {
rpc_err(INVALID_PARAMS_CODE, error.to_string(), None)
}
EthApiError::BothStateAndStateDiffInOverride(_) |
EthApiError::InvalidTracerConfig => invalid_params_rpc_err(error.to_string()),
EthApiError::InvalidTransaction(err) => err.into(),
EthApiError::PoolError(_) |
EthApiError::PrevrandaoNotSet |
EthApiError::InvalidBlockData(_) |
EthApiError::Internal(_) => internal_rpc_err(error.to_string()),
EthApiError::Internal(_) |
EthApiError::TransactionNotFound => internal_rpc_err(error.to_string()),
EthApiError::Unsupported(msg) => internal_rpc_err(msg),
}
}
}

View File

@ -125,6 +125,11 @@ impl ToRpcResultExt for RethResult<Option<Block>> {
}
}
/// Constructs an invalid params JSON-RPC error.
pub(crate) fn invalid_params_rpc_err(msg: impl Into<String>) -> jsonrpsee::core::Error {
rpc_err(jsonrpsee::types::error::INVALID_PARAMS_CODE, msg, None)
}
/// Constructs an internal JSON-RPC error.
pub(crate) fn internal_rpc_err(msg: impl Into<String>) -> jsonrpsee::core::Error {
rpc_err(jsonrpsee::types::error::INTERNAL_ERROR_CODE, msg, None)

View File

@ -9,7 +9,7 @@ use reth_provider::{BlockProvider, EvmEnvProvider, StateProviderFactory};
use reth_revm::{
database::{State, SubState},
env::tx_env_with_recovered,
tracing::{TraceInspectorConfig, TracingInspector},
tracing::{TracingInspector, TracingInspectorConfig},
};
use reth_rpc_api::TraceApiServer;
use reth_rpc_types::{
@ -82,7 +82,7 @@ where
let tx = tx_env_with_recovered(&tx);
let env = Env { cfg, block, tx };
let db = SubState::new(State::new(state));
let mut inspector = TracingInspector::new(TraceInspectorConfig::default_parity());
let mut inspector = TracingInspector::new(TracingInspectorConfig::default_parity());
inspect(db, env, &mut inspector)?;