diff --git a/prdoc/pr_9722.prdoc b/prdoc/pr_9722.prdoc new file mode 100644 index 0000000000000..e267c3baa3157 --- /dev/null +++ b/prdoc/pr_9722.prdoc @@ -0,0 +1,109 @@ +title: '[pallet-revive] opcode tracer' +doc: +- audience: Runtime Dev + description: | + This PR introduces a **Geth-compatible execution tracer** ([StructLogger](https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers#struct-opcode-logger)) for pallet-revive + + The tracer can be used to capture both EVM opcode and PVM syscall. + It can be used with the same RPC endpoint as Geth StructLogger. + + + Since it can be quite resource intensive, It can only be queried from the node when the **DebugSettings** are enabled (This is turned on now by default in the dev-node) + + Tested in https://github.com/paritytech/evm-test-suite/pull/138 + + + example: + + ```sh + ❯ cast rpc debug_traceTransaction "" | jq + + # or with options + # See list of options https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers#struct-opcode-logger + + ❯ cast rpc debug_traceTransaction "", { "tracer": { "enableMemory": true } } | jq + ``` + + The response includes additional fields compared to the original Geth debug RPC endpoints: + + For the trace: + - `weight_consumed`: same as gas but expressed in Weight + - `base_call_weight`: the base cost of the transaction + + For each step: + - `weight_cost`: same as gas_cost but expressed in Weight + + For an EVM execution, the output will look like this + + ```json + { + "gas": 4208049, + "weight_consumed": { "ref_time": 126241470000, "proof_size": 4208 }, + "base_call_weight": { "ref_time": 9000000000, "proof_size": 3000 }, + "failed": false, + "returnValue": "0x", + "structLogs": [ + { + "gas": 4109533, + "gasCost": 3, + "weight_cost": { "ref_time": 90000, "proof_size": 0 }, + "depth": 1, + "pc": 0, + "op": "PUSH1", + "stack": [] + }, + { + "gas": 4109530, + "gasCost": 3, + "weight_cost": { "ref_time": 90000, "proof_size": 0 }, + "depth": 1, + "pc": 2, + "op": "PUSH1", + "stack": [ + "0x80" + ] + }, + { + "gas": 4109527, + "gasCost": 3, + "weight_cost": { "ref_time": 90000, "proof_size": 0 }, + "depth": 1, + "pc": 4, + "op": "MSTORE", + "stack": [ + "0x80", + "0x40" + ] + }] + } + ``` + + For PVM execution, each step includes additional fields not present in Geth: + + - `args`: Array of syscall arguments (register values a0-a5) as hex strings + - `returned`: The syscall return value + + These fields are enabled by default. To disable them, use `disableSyscallDetails: true`. + + Example output with syscall details: + + ```json + { + "gas": 97108, + "gasCost": 131, + "weight_cost": { "ref_time": 3930000, "proof_size": 0 }, + "depth": 1, + "op": "call_data_load", + "args": ["0x0", "0x4"], + "returned": "0x2a" + } + ``` +crates: +- name: pallet-revive + bump: patch +- name: pallet-revive-eth-rpc + bump: patch +- name: pallet-revive-proc-macro + bump: patch +- name: revive-dev-runtime + bump: patch diff --git a/substrate/frame/revive/dev-node/node/src/command.rs b/substrate/frame/revive/dev-node/node/src/command.rs index b38869a38eee8..6767f0fed55cd 100644 --- a/substrate/frame/revive/dev-node/node/src/command.rs +++ b/substrate/frame/revive/dev-node/node/src/command.rs @@ -125,6 +125,10 @@ pub fn run_with_args(args: Vec) -> sc_cli::Result<()> { // Enforce dev cli.run.shared_params.dev = true; + // Increase max_response_size for large trace responses + cli.run.rpc_params.rpc_max_response_size = + cli.run.rpc_params.rpc_max_response_size.max(50); + // Pass Default logging settings if none are specified if std::env::var("RUST_LOG").is_err() && cli.run.shared_params.log.is_empty() { cli.run.shared_params.log = "error,sc_rpc_server=info,runtime::revive=debug" diff --git a/substrate/frame/revive/dev-node/runtime/src/lib.rs b/substrate/frame/revive/dev-node/runtime/src/lib.rs index 176d6b91a13c3..086b4b3a77f69 100644 --- a/substrate/frame/revive/dev-node/runtime/src/lib.rs +++ b/substrate/frame/revive/dev-node/runtime/src/lib.rs @@ -355,7 +355,7 @@ impl pallet_revive::Config for Runtime { type InstantiateOrigin = EnsureSigned; type Time = Timestamp; type FeeInfo = FeeInfo; - type DebugEnabled = ConstBool; + type DebugEnabled = ConstBool; type GasScale = ConstU32<50000>; } diff --git a/substrate/frame/revive/proc-macro/src/lib.rs b/substrate/frame/revive/proc-macro/src/lib.rs index 734ad9b488c1c..1eab47cf53b72 100644 --- a/substrate/frame/revive/proc-macro/src/lib.rs +++ b/substrate/frame/revive/proc-macro/src/lib.rs @@ -88,6 +88,15 @@ impl HostFnReturn { Self::ReturnCode => parse_quote! { -> ReturnErrorCode }, } } + + fn trace_return_value(&self) -> TokenStream2 { + match self { + Self::Unit => quote! { None }, + Self::U32 => quote! { result.as_ref().ok().map(|r| *r as u64) }, + Self::ReturnCode => quote! { result.as_ref().ok().copied().map(u64::from) }, + Self::U64 => quote! { result.as_ref().ok().copied() }, + } + } } impl EnvDef { @@ -317,12 +326,19 @@ fn expand_env(def: &EnvDef) -> TokenStream2 { let bench_impls = expand_bench_functions(def); let docs = expand_func_doc(def); let all_syscalls = expand_func_list(def); + let lookup_syscall = expand_func_lookup(def); quote! { + /// Returns the list of all syscalls. pub fn list_syscalls() -> &'static [&'static [u8]] { #all_syscalls } + /// Return the index of a syscall in the `all_syscalls()` list. + pub fn lookup_syscall_index(name: &'static str) -> Option { + #lookup_syscall + } + impl<'a, E: Ext, M: PolkaVmInstance> Runtime<'a, E, M> { fn handle_ecall( &mut self, @@ -377,10 +393,10 @@ fn expand_functions(def: &EnvDef) -> TokenStream2 { let syscall_symbol = Literal::byte_string(name.as_bytes()); let body = &f.item.block; let map_output = f.returns.map_output(); + let trace_return = f.returns.trace_return_value(); let output = &f.item.sig.output; // wrapped host function body call with host function traces - // see https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/contracts#host-function-tracing let wrapped_body_with_trace = { let trace_fmt_args = params.clone().filter_map(|arg| match arg { syn::FnArg::Receiver(_) => None, @@ -396,11 +412,18 @@ fn expand_functions(def: &EnvDef) -> TokenStream2 { .collect::>() .join(", "); let trace_fmt_str = format!("{}({}) = {{:?}} weight_consumed: {{:?}}", name, params_fmt_str); + let trace_args_for_tracer: Vec<_> = trace_fmt_args.clone().collect(); quote! { + crate::tracing::if_tracing(|tracer| { + tracer.enter_ecall(#name, &[#( #trace_args_for_tracer as u64 ),*], self) + }); + // wrap body in closure to make sure the tracing is always executed let result = (|| #body)(); ::log::trace!(target: "runtime::revive::strace", #trace_fmt_str, #( #trace_fmt_args, )* result, self.ext.frame_meter().weight_consumed()); + + crate::tracing::if_tracing(|tracer| tracer.exit_step(self, #trace_return)); result } }; @@ -519,3 +542,19 @@ fn expand_func_list(def: &EnvDef) -> TokenStream2 { } } } + +fn expand_func_lookup(def: &EnvDef) -> TokenStream2 { + let arms = def.host_funcs.iter().enumerate().map(|(idx, f)| { + let name_str = &f.name; + quote! { + #name_str => Some(#idx as u8) + } + }); + + quote! { + match name { + #( #arms, )* + _ => None, + } + } +} diff --git a/substrate/frame/revive/rpc/examples/deploy.rs b/substrate/frame/revive/rpc/examples/deploy.rs index 12f539bba42ff..bcea0fc280c1e 100644 --- a/substrate/frame/revive/rpc/examples/deploy.rs +++ b/substrate/frame/revive/rpc/examples/deploy.rs @@ -39,7 +39,7 @@ async fn main() -> anyhow::Result<()> { println!("\n\n=== Deploying contract ===\n\n"); let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?; - let tx = TransactionBuilder::new(&client) + let tx = TransactionBuilder::new(client.clone()) .value(5_000_000_000_000u128.into()) .input(input) .send() @@ -65,7 +65,7 @@ async fn main() -> anyhow::Result<()> { } println!("\n\n=== Calling contract ===\n\n"); - let tx = TransactionBuilder::new(&client) + let tx = TransactionBuilder::new(client.clone()) .value(U256::from(1_000_000u32)) .to(contract_address) .send() diff --git a/substrate/frame/revive/rpc/examples/eth-rpc-tester.rs b/substrate/frame/revive/rpc/examples/eth-rpc-tester.rs index 9f3c44a3289f0..1fa03ffbe7dbb 100644 --- a/substrate/frame/revive/rpc/examples/eth-rpc-tester.rs +++ b/substrate/frame/revive/rpc/examples/eth-rpc-tester.rs @@ -141,7 +141,7 @@ async fn test_eth_rpc(rpc_url: &str) -> anyhow::Result<()> { println!("- balance: {balance:?}"); println!("\n\n=== Deploying dummy contract ===\n\n"); - let tx = TransactionBuilder::new(&client).input(input).send().await?; + let tx = TransactionBuilder::new(client.clone()).input(input).send().await?; println!("Hash: {:?}", tx.hash()); println!("Waiting for receipt..."); @@ -156,7 +156,7 @@ async fn test_eth_rpc(rpc_url: &str) -> anyhow::Result<()> { println!("- Address: {contract_address:?}"); println!("\n\n=== Calling dummy contract ===\n\n"); - let tx = TransactionBuilder::new(&client).to(contract_address).send().await?; + let tx = TransactionBuilder::new(client.clone()).to(contract_address).send().await?; println!("Hash: {:?}", tx.hash()); println!("Waiting for receipt..."); diff --git a/substrate/frame/revive/rpc/examples/transfer.rs b/substrate/frame/revive/rpc/examples/transfer.rs index e4eb27aabfb36..12823d4a34827 100644 --- a/substrate/frame/revive/rpc/examples/transfer.rs +++ b/substrate/frame/revive/rpc/examples/transfer.rs @@ -39,7 +39,7 @@ async fn main() -> anyhow::Result<()> { print_balance().await?; println!("\n\n=== Transferring ===\n\n"); - let tx = TransactionBuilder::new(&client) + let tx = TransactionBuilder::new(client.clone()) .signer(alith) .value(value) .to(ethan.address()) diff --git a/substrate/frame/revive/rpc/examples/tx-types.rs b/substrate/frame/revive/rpc/examples/tx-types.rs index a3467d4f4933c..ec6dccf401b20 100644 --- a/substrate/frame/revive/rpc/examples/tx-types.rs +++ b/substrate/frame/revive/rpc/examples/tx-types.rs @@ -35,7 +35,7 @@ async fn main() -> anyhow::Result<()> { ] { println!("\n\n=== TransactionType {tx_type:?} ===\n\n",); - let tx = TransactionBuilder::new(&client) + let tx = TransactionBuilder::new(client.clone()) .signer(alith.clone()) .value(value) .to(ethan.address()) diff --git a/substrate/frame/revive/rpc/src/apis/debug_apis.rs b/substrate/frame/revive/rpc/src/apis/debug_apis.rs index 6704d74b13f8d..3eb1822fb8b64 100644 --- a/substrate/frame/revive/rpc/src/apis/debug_apis.rs +++ b/substrate/frame/revive/rpc/src/apis/debug_apis.rs @@ -29,7 +29,7 @@ pub trait DebugRpc { async fn trace_block_by_number( &self, block: BlockNumberOrTag, - tracer_config: TracerConfig, + tracer_config: Option, ) -> RpcResult>; /// Returns a transaction's traces by replaying it. @@ -41,7 +41,7 @@ pub trait DebugRpc { async fn trace_transaction( &self, transaction_hash: H256, - tracer_config: TracerConfig, + tracer_config: Option, ) -> RpcResult; /// Dry run a call and returns the transaction's traces. @@ -54,7 +54,7 @@ pub trait DebugRpc { &self, transaction: GenericTransaction, block: BlockNumberOrTagOrHash, - tracer_config: TracerConfig, + tracer_config: Option, ) -> RpcResult; #[method(name = "debug_getAutomine")] @@ -94,18 +94,18 @@ impl DebugRpcServer for DebugRpcServerImpl { async fn trace_block_by_number( &self, block: BlockNumberOrTag, - tracer_config: TracerConfig, + tracer_config: Option, ) -> RpcResult> { - let TracerConfig { config, timeout } = tracer_config; + let TracerConfig { config, timeout } = tracer_config.unwrap_or_default(); with_timeout(timeout, self.client.trace_block_by_number(block, config)).await } async fn trace_transaction( &self, transaction_hash: H256, - tracer_config: TracerConfig, + tracer_config: Option, ) -> RpcResult { - let TracerConfig { config, timeout } = tracer_config; + let TracerConfig { config, timeout } = tracer_config.unwrap_or_default(); with_timeout(timeout, self.client.trace_transaction(transaction_hash, config)).await } @@ -113,9 +113,9 @@ impl DebugRpcServer for DebugRpcServerImpl { &self, transaction: GenericTransaction, block: BlockNumberOrTagOrHash, - tracer_config: TracerConfig, + tracer_config: Option, ) -> RpcResult { - let TracerConfig { config, timeout } = tracer_config; + let TracerConfig { config, timeout } = tracer_config.unwrap_or_default(); with_timeout(timeout, self.client.trace_call(transaction, block, config)).await } diff --git a/substrate/frame/revive/rpc/src/cli.rs b/substrate/frame/revive/rpc/src/cli.rs index aed59c6acd660..15d1400c28a13 100644 --- a/substrate/frame/revive/rpc/src/cli.rs +++ b/substrate/frame/revive/rpc/src/cli.rs @@ -111,10 +111,12 @@ fn build_client( earliest_receipt_block: Option, node_rpc_url: &str, database_url: &str, + max_request_size: u32, + max_response_size: u32, abort_signal: Signals, ) -> anyhow::Result { let fut = async { - let (api, rpc_client, rpc) = connect(node_rpc_url).await?; + let (api, rpc_client, rpc) = connect(node_rpc_url, max_request_size, max_response_size).await?; let block_provider = SubxtBlockInfoProvider::new( api.clone(), rpc.clone()).await?; let (pool, keep_latest_n_blocks) = if database_url == IN_MEMORY_DB { @@ -213,6 +215,8 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> { earliest_receipt_block, &node_rpc_url, &database_url, + rpc_config.max_request_size * 1024 * 1024, + rpc_config.max_response_size * 1024 * 1024, tokio_runtime.block_on(async { Signals::capture() })?, )?; diff --git a/substrate/frame/revive/rpc/src/client.rs b/substrate/frame/revive/rpc/src/client.rs index 908adebd3e2a5..e1c9aae495fa6 100644 --- a/substrate/frame/revive/rpc/src/client.rs +++ b/substrate/frame/revive/rpc/src/client.rs @@ -209,11 +209,15 @@ async fn get_automine(rpc_client: &RpcClient) -> bool { /// clients. pub async fn connect( node_rpc_url: &str, + max_request_size: u32, + max_response_size: u32, ) -> Result<(OnlineClient, RpcClient, LegacyRpcMethods), ClientError> { log::info!(target: LOG_TARGET, "🌐 Connecting to node at: {node_rpc_url} ..."); let rpc_client = ReconnectingRpcClient::builder() .retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10))) + .max_request_size(max_request_size) + .max_response_size(max_response_size) .build(node_rpc_url.to_string()) .await?; let rpc_client = RpcClient::new(rpc_client); diff --git a/substrate/frame/revive/rpc/src/example.rs b/substrate/frame/revive/rpc/src/example.rs index 4b5208c5e94b4..e4a439cf45c2e 100644 --- a/substrate/frame/revive/rpc/src/example.rs +++ b/substrate/frame/revive/rpc/src/example.rs @@ -88,9 +88,9 @@ impl SubmittedTransaction { } impl TransactionBuilder { - pub fn new(client: &Arc) -> Self { + pub fn new(client: Arc) -> Self { Self { - client: Arc::clone(client), + client, signer: Account::default(), value: U256::zero(), input: Bytes::default(), diff --git a/substrate/frame/revive/rpc/src/tests.rs b/substrate/frame/revive/rpc/src/tests.rs index 269dffb8c7572..fbf9d7e9a75d2 100644 --- a/substrate/frame/revive/rpc/src/tests.rs +++ b/substrate/frame/revive/rpc/src/tests.rs @@ -20,7 +20,6 @@ use crate::{ cli::{self, CliCommand}, - client, example::TransactionBuilder, subxt_client::{ self, src_chain::runtime_types::pallet_revive::primitives::Code, SrcChainConfig, @@ -103,6 +102,10 @@ impl SharedResources { ws_client_with_retry("ws://localhost:45788").await } + async fn node_client() -> OnlineClient { + OnlineClient::::from_url(Self::node_rpc_url()).await.unwrap() + } + fn node_rpc_url() -> &'static str { "ws://localhost:45789" } @@ -120,7 +123,7 @@ macro_rules! unwrap_call_err( // Helper functions /// Prepare multiple EVM transfer transactions with nonce in descending order async fn prepare_evm_transactions( - client: &Arc, + client: Arc, signer: Account, recipient: pallet_revive::evm::Address, amount: U256, @@ -132,7 +135,7 @@ async fn prepare_evm_transactions( let mut transactions = Vec::new(); for i in (0..count).rev() { let nonce = start_nonce.saturating_add(U256::from(i as u64)); - let tx_builder = TransactionBuilder::new(client) + let tx_builder = TransactionBuilder::new(client.clone()) .signer(signer.clone()) .nonce(nonce) .value(amount) @@ -264,9 +267,24 @@ async fn verify_transactions_in_single_block( #[tokio::test] async fn run_all_eth_rpc_tests() -> anyhow::Result<()> { + // Set up a 2-minute timeout for the entire test + let timeout_duration = tokio::time::Duration::from_secs(120); + let result = tokio::time::timeout(timeout_duration, run_all_eth_rpc_tests_inner()).await; + + match result { + Ok(inner_result) => inner_result, + Err(_) => { + log::error!(target: LOG_TARGET, "Test timed out after 2 minutes!"); + std::process::exit(1); + }, + } +} + +async fn run_all_eth_rpc_tests_inner() -> anyhow::Result<()> { // start node and rpc server let _shared = SharedResources::start(); - let client = Arc::new(SharedResources::client().await); + // Wait for servers to be ready + let _ = SharedResources::client().await; macro_rules! run_tests { ($($test:ident),+ $(,)?) => { @@ -274,7 +292,7 @@ async fn run_all_eth_rpc_tests() -> anyhow::Result<()> { { let test_name = stringify!($test); log::debug!(target: LOG_TARGET, "Running test: {}", test_name); - match $test(client.clone()).await { + match $test().await { Ok(()) => log::debug!(target: LOG_TARGET, "Test passed: {}", test_name), Err(err) => panic!("Test {} failed: {err:?}", test_name), } @@ -303,12 +321,17 @@ async fn run_all_eth_rpc_tests() -> anyhow::Result<()> { Ok(()) } -async fn test_transfer(client: Arc) -> anyhow::Result<()> { +async fn test_transfer() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); let ethan = Account::from(subxt_signer::eth::dev::ethan()); let initial_balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?; let value = 1_000_000_000_000_000_000_000u128.into(); - let tx = TransactionBuilder::new(&client).value(value).to(ethan.address()).send().await?; + let tx = TransactionBuilder::new(client.clone()) + .value(value) + .to(ethan.address()) + .send() + .await?; let receipt = tx.wait_for_receipt().await?; assert_eq!( @@ -327,14 +350,19 @@ async fn test_transfer(client: Arc) -> anyhow::Result<()> { Ok(()) } -async fn test_deploy_and_call(client: Arc) -> anyhow::Result<()> { +async fn test_deploy_and_call() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); let account = Account::default(); // Balance transfer let ethan = Account::from(subxt_signer::eth::dev::ethan()); let initial_balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?; let value = 1_000_000_000_000_000_000_000u128.into(); - let tx = TransactionBuilder::new(&client).value(value).to(ethan.address()).send().await?; + let tx = TransactionBuilder::new(client.clone()) + .value(value) + .to(ethan.address()) + .send() + .await?; let receipt = tx.wait_for_receipt().await?; assert_eq!( @@ -357,7 +385,7 @@ async fn test_deploy_and_call(client: Arc) -> anyhow::Result<()> { let (bytes, _) = pallet_revive_fixtures::compile_module("dummy")?; let input = bytes.into_iter().chain(data.clone()).collect::>(); let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?; - let tx = TransactionBuilder::new(&client).value(value).input(input).send().await?; + let tx = TransactionBuilder::new(client.clone()).value(value).input(input).send().await?; let receipt = tx.wait_for_receipt().await?; let contract_address = create1(&account.address(), nonce.try_into().unwrap()); assert_eq!( @@ -378,7 +406,7 @@ async fn test_deploy_and_call(client: Arc) -> anyhow::Result<()> { ); // Call contract - let tx = TransactionBuilder::new(&client) + let tx = TransactionBuilder::new(client.clone()) .value(value) .to(contract_address) .send() @@ -396,7 +424,7 @@ async fn test_deploy_and_call(client: Arc) -> anyhow::Result<()> { // Balance transfer to contract let initial_balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?; - let tx = TransactionBuilder::new(&client) + let tx = TransactionBuilder::new(client.clone()) .value(value) .to(contract_address) .send() @@ -414,7 +442,9 @@ async fn test_deploy_and_call(client: Arc) -> anyhow::Result<()> { Ok(()) } -async fn test_runtime_api_dry_run_addr_works(client: Arc) -> anyhow::Result<()> { +async fn test_runtime_api_dry_run_addr_works() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); + let node_client = SharedResources::node_client().await; let account = Account::default(); let origin: [u8; 32] = account.substrate_account().into(); let data = b"hello world".to_vec(); @@ -437,17 +467,24 @@ async fn test_runtime_api_dry_run_addr_works(client: Arc) -> anyhow::R .await?; let contract_address = create1(&account.address(), nonce.try_into().unwrap()); - let c = OnlineClient::::from_url("ws://localhost:45789").await?; - let res = c.runtime_api().at_latest().await?.call(payload).await?.result.unwrap(); + let res = node_client + .runtime_api() + .at_latest() + .await? + .call(payload) + .await? + .result + .unwrap(); assert_eq!(res.addr, contract_address); Ok(()) } -async fn test_invalid_transaction(client: Arc) -> anyhow::Result<()> { +async fn test_invalid_transaction() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); let ethan = Account::from(subxt_signer::eth::dev::ethan()); - let err = TransactionBuilder::new(&client) + let err = TransactionBuilder::new(client.clone()) .value(U256::from(1_000_000_000_000u128)) .to(ethan.address()) .mutate(|tx| match tx { @@ -484,14 +521,15 @@ async fn get_evm_block_from_storage( Ok(block.0) } -async fn test_evm_blocks_should_match(client: Arc) -> anyhow::Result<()> { - let (node_client, node_rpc_client, _) = - client::connect(SharedResources::node_rpc_url()).await.unwrap(); +async fn test_evm_blocks_should_match() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); + let node_client = SharedResources::node_client().await; + let node_rpc_client = RpcClient::from_url(SharedResources::node_rpc_url()).await?; // Deploy a contract to have some interesting blocks let (bytes, _) = pallet_revive_fixtures::compile_module("dummy")?; let value = U256::from(5_000_000_000_000u128); - let tx = TransactionBuilder::new(&client) + let tx = TransactionBuilder::new(client.clone()) .value(value) .input(bytes.to_vec()) .send() @@ -529,13 +567,14 @@ async fn test_evm_blocks_should_match(client: Arc) -> anyhow::Result<( Ok(()) } -async fn test_evm_blocks_hydrated_should_match(client: Arc) -> anyhow::Result<()> { +async fn test_evm_blocks_hydrated_should_match() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); // Deploy a contract to have some transactions in the block let (bytes, _) = pallet_revive_fixtures::compile_module("dummy")?; let value = U256::from(5_000_000_000_000u128); let signer = Account::default(); let signer_copy = Account::default(); - let tx = TransactionBuilder::new(&client) + let tx = TransactionBuilder::new(client.clone()) .value(value) .signer(signer) .input(bytes.to_vec()) @@ -582,13 +621,12 @@ async fn test_evm_blocks_hydrated_should_match(client: Arc) -> anyhow: Ok(()) } -async fn test_block_hash_for_tag_with_proper_ethereum_block_hash_works( - client: Arc, -) -> anyhow::Result<()> { +async fn test_block_hash_for_tag_with_proper_ethereum_block_hash_works() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); // Deploy a transaction to create a block with transactions let (bytes, _) = pallet_revive_fixtures::compile_module("dummy")?; let value = U256::from(5_000_000_000_000u128); - let tx = TransactionBuilder::new(&client) + let tx = TransactionBuilder::new(client.clone()) .value(value) .input(bytes.to_vec()) .send() @@ -613,9 +651,8 @@ async fn test_block_hash_for_tag_with_proper_ethereum_block_hash_works( Ok(()) } -async fn test_block_hash_for_tag_with_invalid_ethereum_block_hash_fails( - client: Arc, -) -> anyhow::Result<()> { +async fn test_block_hash_for_tag_with_invalid_ethereum_block_hash_fails() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); let fake_eth_hash = H256::from([0x42u8; 32]); log::trace!(target: LOG_TARGET, "Testing with fake Ethereum hash: {fake_eth_hash:?}"); @@ -628,9 +665,8 @@ async fn test_block_hash_for_tag_with_invalid_ethereum_block_hash_fails( Ok(()) } -async fn test_block_hash_for_tag_with_block_number_works( - client: Arc, -) -> anyhow::Result<()> { +async fn test_block_hash_for_tag_with_block_number_works() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); let block_number = client.block_number().await?; log::trace!(target: LOG_TARGET, "Testing with block number: {block_number}"); @@ -644,9 +680,8 @@ async fn test_block_hash_for_tag_with_block_number_works( Ok(()) } -async fn test_block_hash_for_tag_with_block_tags_works( - client: Arc, -) -> anyhow::Result<()> { +async fn test_block_hash_for_tag_with_block_tags_works() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); let account = Account::default(); let tags = vec![ @@ -666,7 +701,8 @@ async fn test_block_hash_for_tag_with_block_tags_works( Ok(()) } -async fn test_multiple_transactions_in_block(client: Arc) -> anyhow::Result<()> { +async fn test_multiple_transactions_in_block() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); let num_transactions = 20; let alith = Account::default(); let ethan = Account::from(subxt_signer::eth::dev::ethan()); @@ -674,7 +710,8 @@ async fn test_multiple_transactions_in_block(client: Arc) -> anyhow::R // Prepare EVM transfer transactions let transactions = - prepare_evm_transactions(&client, alith, ethan.address(), amount, num_transactions).await?; + prepare_evm_transactions(client.clone(), alith, ethan.address(), amount, num_transactions) + .await?; // Submit all transactions let submitted_txs = submit_evm_transactions(transactions).await?; @@ -689,7 +726,9 @@ async fn test_multiple_transactions_in_block(client: Arc) -> anyhow::R Ok(()) } -async fn test_mixed_evm_substrate_transactions(client: Arc) -> anyhow::Result<()> { +async fn test_mixed_evm_substrate_transactions() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); + let node_client = SharedResources::node_client().await; let num_evm_txs = 10; let num_substrate_txs = 7; @@ -700,12 +739,12 @@ async fn test_mixed_evm_substrate_transactions(client: Arc) -> anyhow: // Prepare EVM transactions log::trace!(target: LOG_TARGET, "Creating {num_evm_txs} EVM transfer transactions"); let evm_transactions = - prepare_evm_transactions(&client, alith, ethan.address(), amount, num_evm_txs).await?; + prepare_evm_transactions(client.clone(), alith, ethan.address(), amount, num_evm_txs) + .await?; // Prepare substrate transactions (simple remarks) log::trace!(target: LOG_TARGET, "Creating {num_substrate_txs} substrate remark transactions"); let alice_signer = subxt_signer::sr25519::dev::alice(); - let (node_client, _, _) = client::connect(SharedResources::node_rpc_url()).await.unwrap(); let substrate_txs = prepare_substrate_transactions(&node_client, &alice_signer, num_substrate_txs).await?; @@ -734,9 +773,10 @@ async fn test_mixed_evm_substrate_transactions(client: Arc) -> anyhow: Ok(()) } -async fn test_runtime_pallets_address_upload_code(client: Arc) -> anyhow::Result<()> { - let (node_client, node_rpc_client, _) = - client::connect(SharedResources::node_rpc_url()).await?; +async fn test_runtime_pallets_address_upload_code() -> anyhow::Result<()> { + let client = Arc::new(SharedResources::client().await); + let node_client = SharedResources::node_client().await; + let node_rpc_client = RpcClient::from_url(SharedResources::node_rpc_url()).await?; let (bytecode, _) = pallet_revive_fixtures::compile_module("dummy")?; let signer = Account::default(); @@ -763,7 +803,7 @@ async fn test_runtime_pallets_address_upload_code(client: Arc) -> anyh let encoded_call = node_client.tx().call_data(&upload_call)?; // Step 2: Send the encoded call to RUNTIME_PALLETS_ADDR - let tx = TransactionBuilder::new(&client) + let tx = TransactionBuilder::new(client.clone()) .signer(signer.clone()) .to(pallet_revive::RUNTIME_PALLETS_ADDR) .input(encoded_call.clone()) diff --git a/substrate/frame/revive/src/debug.rs b/substrate/frame/revive/src/debug.rs index 6d569523459c0..95af377659aed 100644 --- a/substrate/frame/revive/src/debug.rs +++ b/substrate/frame/revive/src/debug.rs @@ -43,11 +43,25 @@ pub struct DebugSettings { bypass_eip_3607: bool, /// Whether to enable PolkaVM logs. pvm_logs: bool, + /// Whether to disable execution tracing. + disable_execution_tracing: bool, } impl DebugSettings { - pub fn new(allow_unlimited_contract_size: bool, bypass_eip_3607: bool, pvm_logs: bool) -> Self { - Self { allow_unlimited_contract_size, bypass_eip_3607, pvm_logs } + #[cfg(test)] + pub fn set_bypass_eip_3607(mut self, value: bool) -> Self { + self.bypass_eip_3607 = value; + self + } + + #[cfg(test)] + pub fn set_allow_unlimited_contract_size(mut self, value: bool) -> Self { + self.allow_unlimited_contract_size = value; + self + } + + pub fn is_execution_tracing_enabled() -> bool { + T::DebugEnabled::get() && !DebugSettingsOf::::get().disable_execution_tracing } /// Returns true if unlimited contract size is allowed. diff --git a/substrate/frame/revive/src/evm/api/byte.rs b/substrate/frame/revive/src/evm/api/byte.rs index 1d3de2011ae22..d598eaf385e5c 100644 --- a/substrate/frame/revive/src/evm/api/byte.rs +++ b/substrate/frame/revive/src/evm/api/byte.rs @@ -75,6 +75,17 @@ impl Bytes { pub fn is_empty(&self) -> bool { self.0.is_empty() } + + /// Convert to minimal hex format without padding 0s + pub fn to_short_hex(&self) -> alloc::string::String { + let word = sp_core::U256::from_big_endian(&self.0); + alloc::format!("0x{:x}", word) + } + + /// Convert to hex format without "0x" prefix + pub fn to_hex_no_prefix(&self) -> alloc::string::String { + alloy_core::hex::encode(&self.0) + } } impl_hex!(Byte, u8, 0u8); @@ -83,6 +94,18 @@ impl_hex!(Bytes8, [u8; 8], [0u8; 8]); impl_hex!(Bytes32, [u8; 32], [0u8; 32]); impl_hex!(Bytes256, [u8; 256], [0u8; 256]); +#[test] +fn test_to_short_hex() { + let bytes = Bytes(crate::U256::from(4).to_big_endian().to_vec()); + assert_eq!(bytes.to_short_hex(), "0x4"); +} + +#[test] +fn test_to_hex_no_prefix() { + let bytes = Bytes(vec![0x12, 0x34, 0x56, 0x78]); + assert_eq!(bytes.to_hex_no_prefix(), "12345678"); +} + #[test] fn serialize_works() { let a = Byte(42); diff --git a/substrate/frame/revive/src/evm/api/debug_rpc_types.rs b/substrate/frame/revive/src/evm/api/debug_rpc_types.rs index 866454e3e8fbc..0c157af11c156 100644 --- a/substrate/frame/revive/src/evm/api/debug_rpc_types.rs +++ b/substrate/frame/revive/src/evm/api/debug_rpc_types.rs @@ -15,13 +15,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::evm::Bytes; +use crate::{evm::Bytes, Weight}; use alloc::{collections::BTreeMap, string::String, vec::Vec}; use codec::{Decode, Encode}; use derive_more::From; use scale_info::TypeInfo; use serde::{ - de::{Error, MapAccess, Visitor}, + de::{Deserializer, Error, MapAccess, Visitor}, ser::{SerializeMap, Serializer}, Deserialize, Serialize, }; @@ -36,6 +36,9 @@ pub enum TracerType { /// A tracer that traces the prestate. PrestateTracer(Option), + + /// A tracer that traces opcodes and syscalls. + ExecutionTracer(Option), } impl From for TracerType { @@ -44,15 +47,27 @@ impl From for TracerType { } } +impl From for TracerType { + fn from(config: PrestateTracerConfig) -> Self { + TracerType::PrestateTracer(Some(config)) + } +} + +impl From for TracerType { + fn from(config: ExecutionTracerConfig) -> Self { + TracerType::ExecutionTracer(Some(config)) + } +} + impl Default for TracerType { fn default() -> Self { - TracerType::CallTracer(Some(CallTracerConfig::default())) + TracerType::ExecutionTracer(Some(ExecutionTracerConfig::default())) } } /// Tracer configuration used to trace calls. #[derive(TypeInfo, Debug, Clone, Default, PartialEq)] -#[cfg_attr(feature = "std", derive(Deserialize, Serialize), serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "std", derive(Serialize), serde(rename_all = "camelCase"))] pub struct TracerConfig { /// The tracer type. #[cfg_attr(feature = "std", serde(flatten, default))] @@ -63,6 +78,48 @@ pub struct TracerConfig { pub timeout: Option, } +#[cfg(feature = "std")] +impl<'de> Deserialize<'de> for TracerConfig { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct TracerConfigWithType { + #[serde(flatten)] + config: TracerType, + #[serde(with = "humantime_serde", default)] + timeout: Option, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct TracerConfigInline { + #[serde(flatten, default)] + execution_tracer_config: ExecutionTracerConfig, + #[serde(with = "humantime_serde", default)] + timeout: Option, + } + + #[derive(Deserialize)] + #[serde(untagged)] + enum TracerConfigHelper { + WithType(TracerConfigWithType), + Inline(TracerConfigInline), + } + + match TracerConfigHelper::deserialize(deserializer)? { + TracerConfigHelper::WithType(cfg) => + Ok(TracerConfig { config: cfg.config, timeout: cfg.timeout }), + TracerConfigHelper::Inline(cfg) => Ok(TracerConfig { + config: TracerType::ExecutionTracer(Some(cfg.execution_tracer_config)), + timeout: cfg.timeout, + }), + } + } +} + /// The configuration for the call tracer. #[derive(Clone, Debug, Decode, Serialize, Deserialize, Encode, PartialEq, TypeInfo)] #[serde(default, rename_all = "camelCase")] @@ -100,6 +157,58 @@ impl Default for PrestateTracerConfig { } } +fn zero_to_none<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt = Option::::deserialize(deserializer)?; + Ok(match opt { + Some(0) => None, + other => other, + }) +} + +/// The configuration for the execution tracer. +#[derive(Clone, Debug, Decode, Serialize, Deserialize, Encode, PartialEq, TypeInfo)] +#[serde(default, rename_all = "camelCase")] +pub struct ExecutionTracerConfig { + /// Whether to enable memory capture + pub enable_memory: bool, + + /// Whether to disable stack capture + pub disable_stack: bool, + + /// Whether to disable storage capture + pub disable_storage: bool, + + /// Whether to enable return data capture + pub enable_return_data: bool, + + /// Whether to disable syscall details capture, including arguments and return value (PVM only) + pub disable_syscall_details: bool, + + /// Limit number of steps captured + #[serde(skip_serializing_if = "Option::is_none", deserialize_with = "zero_to_none")] + pub limit: Option, + + /// Maximum number of memory words to capture per step (default: 16) + pub memory_word_limit: u32, +} + +impl Default for ExecutionTracerConfig { + fn default() -> Self { + Self { + enable_memory: false, + disable_stack: false, + disable_storage: false, + enable_return_data: false, + disable_syscall_details: false, + limit: None, + memory_word_limit: 16, + } + } +} + /// Serialization should support the following JSON format: /// /// ```json @@ -109,9 +218,38 @@ impl Default for PrestateTracerConfig { /// ```json /// { "tracer": "callTracer" } /// ``` +/// +/// By default if not specified the tracer is an ExecutionTracer, and it's config is passed inline +/// +/// ```json +/// { "tracer": null, "enableMemory": true, "disableStack": false, "disableStorage": false, "enableReturnData": true } +/// ``` #[test] fn test_tracer_config_serialization() { let tracers = vec![ + ( + r#"{ "enableMemory": true, "disableStack": false, "disableStorage": false, + "enableReturnData": true }"#, + TracerConfig { + config: TracerType::ExecutionTracer(Some(ExecutionTracerConfig { + enable_memory: true, + disable_stack: false, + disable_storage: false, + enable_return_data: true, + disable_syscall_details: false, + limit: None, + memory_word_limit: 16, + })), + timeout: None, + }, + ), + ( + r#"{ }"#, + TracerConfig { + config: TracerType::ExecutionTracer(Some(ExecutionTracerConfig::default())), + timeout: None, + }, + ), ( r#"{"tracer": "callTracer"}"#, TracerConfig { config: TracerType::CallTracer(None), timeout: None }, @@ -131,18 +269,37 @@ fn test_tracer_config_serialization() { }, ), ( - r#"{"tracer": "callTracer", "tracerConfig": { "onlyTopCall": true }, "timeout": "10ms"}"#, + r#"{"tracer": "callTracer", "tracerConfig": { "onlyTopCall": true }, "timeout": + "10ms"}"#, TracerConfig { config: CallTracerConfig { with_logs: true, only_top_call: true }.into(), timeout: Some(core::time::Duration::from_millis(10)), }, ), + ( + r#"{"tracer": "executionTracer"}"#, + TracerConfig { config: TracerType::ExecutionTracer(None), timeout: None }, + ), + ( + r#"{"tracer": "executionTracer", "tracerConfig": { "enableMemory": true }}"#, + TracerConfig { + config: ExecutionTracerConfig { enable_memory: true, ..Default::default() }.into(), + timeout: None, + }, + ), + ( + r#"{ "enableMemory": true }"#, + TracerConfig { + config: ExecutionTracerConfig { enable_memory: true, ..Default::default() }.into(), + timeout: None, + }, + ), ]; for (json_data, expected) in tracers { let result: TracerConfig = serde_json::from_str(json_data).expect("Deserialization should succeed"); - assert_eq!(result, expected); + assert_eq!(result, expected, "invalid serialization for {json_data}"); } } @@ -175,6 +332,8 @@ pub enum Trace { Call(CallTrace), /// A prestate trace. Prestate(PrestateTrace), + /// An execution trace (opcodes and syscalls). + Execution(ExecutionTrace), } /// A prestate Trace @@ -318,18 +477,320 @@ where ser_map.end() } +/// An execution trace containing the step-by-step execution of EVM opcodes and PVM syscalls. +/// This matches Geth's structLogger output format. +#[derive( + Default, TypeInfo, Encode, Decode, Serialize, Deserialize, Clone, Debug, Eq, PartialEq, +)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionTrace { + /// Total gas used by the transaction. + pub gas: u64, + /// The weight consumed by the transaction meter. + pub weight_consumed: Weight, + /// The base call weight of the transaction. + pub base_call_weight: Weight, + /// Whether the transaction failed. + pub failed: bool, + /// The return value of the transaction. + pub return_value: Bytes, + /// The list of execution steps (structLogs in Geth). + pub struct_logs: Vec, +} + +/// An execution step which can be either an EVM opcode or a PVM syscall. +#[derive(TypeInfo, Encode, Decode, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionStep { + /// Remaining gas before executing this step. + #[codec(compact)] + pub gas: u64, + /// Gas Cost of executing this step. + #[codec(compact)] + pub gas_cost: u64, + /// Weight cost of executing this step. + pub weight_cost: Weight, + /// Current call depth. + pub depth: u16, + /// Return data from last frame output. + #[serde(skip_serializing_if = "Bytes::is_empty")] + pub return_data: Bytes, + /// Any error that occurred during execution. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// The kind of execution step (EVM opcode or PVM syscall). + #[serde(flatten)] + pub kind: ExecutionStepKind, +} + +/// The kind of execution step. +#[derive(TypeInfo, Encode, Decode, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(untagged)] +pub enum ExecutionStepKind { + /// An EVM opcode execution. + EVMOpcode { + /// The program counter. + #[codec(compact)] + pc: u32, + /// The opcode being executed. + #[serde(serialize_with = "serialize_opcode", deserialize_with = "deserialize_opcode")] + op: u8, + /// EVM stack contents. + #[serde(serialize_with = "serialize_stack_minimal")] + stack: Vec, + /// EVM memory contents. + #[serde( + skip_serializing_if = "Vec::is_empty", + serialize_with = "serialize_memory_no_prefix" + )] + memory: Vec, + /// Contract storage changes. + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_storage_no_prefix" + )] + storage: Option>, + }, + /// A PVM syscall execution. + PVMSyscall { + /// The executed syscall. + #[serde(serialize_with = "serialize_syscall_op")] + op: u8, + /// The syscall arguments (register values a0-a5). + /// Omitted when `disable_syscall_details` is true in ExecutionTracerConfig. + #[serde(skip_serializing_if = "Vec::is_empty", with = "super::hex_serde::vec")] + args: Vec, + /// The syscall return value. + /// Omitted when `disable_syscall_details` is true in ExecutionTracerConfig. + #[serde(skip_serializing_if = "Option::is_none", with = "super::hex_serde::option")] + returned: Option, + }, +} + +macro_rules! define_opcode_functions { + ($($op:ident),* $(,)?) => { + /// Get opcode name from byte value using opcode names + fn get_opcode_name(opcode: u8) -> &'static str { + use revm::bytecode::opcode::*; + match opcode { + $( + $op => stringify!($op), + )* + _ => "INVALID", + } + } + + /// Get opcode byte from name string + fn get_opcode_byte(name: &str) -> Option { + use revm::bytecode::opcode::*; + match name { + $( + stringify!($op) => Some($op), + )* + _ => None, + } + } + }; +} + +define_opcode_functions!( + STOP, + ADD, + MUL, + SUB, + DIV, + SDIV, + MOD, + SMOD, + ADDMOD, + MULMOD, + EXP, + SIGNEXTEND, + LT, + GT, + SLT, + SGT, + EQ, + ISZERO, + AND, + OR, + XOR, + NOT, + BYTE, + SHL, + SHR, + SAR, + KECCAK256, + ADDRESS, + BALANCE, + ORIGIN, + CALLER, + CALLVALUE, + CALLDATALOAD, + CALLDATASIZE, + CALLDATACOPY, + CODESIZE, + CODECOPY, + GASPRICE, + EXTCODESIZE, + EXTCODECOPY, + RETURNDATASIZE, + RETURNDATACOPY, + EXTCODEHASH, + BLOCKHASH, + COINBASE, + TIMESTAMP, + NUMBER, + DIFFICULTY, + GASLIMIT, + CHAINID, + SELFBALANCE, + BASEFEE, + BLOBHASH, + BLOBBASEFEE, + POP, + MLOAD, + MSTORE, + MSTORE8, + SLOAD, + SSTORE, + JUMP, + JUMPI, + PC, + MSIZE, + GAS, + JUMPDEST, + TLOAD, + TSTORE, + MCOPY, + PUSH0, + PUSH1, + PUSH2, + PUSH3, + PUSH4, + PUSH5, + PUSH6, + PUSH7, + PUSH8, + PUSH9, + PUSH10, + PUSH11, + PUSH12, + PUSH13, + PUSH14, + PUSH15, + PUSH16, + PUSH17, + PUSH18, + PUSH19, + PUSH20, + PUSH21, + PUSH22, + PUSH23, + PUSH24, + PUSH25, + PUSH26, + PUSH27, + PUSH28, + PUSH29, + PUSH30, + PUSH31, + PUSH32, + DUP1, + DUP2, + DUP3, + DUP4, + DUP5, + DUP6, + DUP7, + DUP8, + DUP9, + DUP10, + DUP11, + DUP12, + DUP13, + DUP14, + DUP15, + DUP16, + SWAP1, + SWAP2, + SWAP3, + SWAP4, + SWAP5, + SWAP6, + SWAP7, + SWAP8, + SWAP9, + SWAP10, + SWAP11, + SWAP12, + SWAP13, + SWAP14, + SWAP15, + SWAP16, + LOG0, + LOG1, + LOG2, + LOG3, + LOG4, + CREATE, + CALL, + CALLCODE, + RETURN, + DELEGATECALL, + CREATE2, + STATICCALL, + REVERT, + INVALID, + SELFDESTRUCT, +); + +/// Serialize opcode as string using REVM opcode names +fn serialize_opcode(opcode: &u8, serializer: S) -> Result +where + S: serde::Serializer, +{ + let name = get_opcode_name(*opcode); + serializer.serialize_str(name) +} + +/// Serialize a syscall index to its name +fn serialize_syscall_op(idx: &u8, serializer: S) -> Result +where + S: serde::Serializer, +{ + use crate::vm::pvm::env::list_syscalls; + let Some(syscall_name_bytes) = list_syscalls().get(*idx as usize) else { + return Err(serde::ser::Error::custom(alloc::format!("Unknown syscall: {idx}"))) + }; + let name = core::str::from_utf8(syscall_name_bytes).unwrap_or_default(); + serializer.serialize_str(name) +} + +/// Deserialize opcode from string using reverse lookup table +fn deserialize_opcode<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + get_opcode_byte(&s) + .ok_or_else(|| serde::de::Error::custom(alloc::format!("Unknown opcode: {}", s))) +} + /// A smart contract execution call trace. #[derive( TypeInfo, Default, Encode, Decode, Serialize, Deserialize, Clone, Debug, Eq, PartialEq, )] #[serde(rename_all = "camelCase")] -pub struct CallTrace { +pub struct CallTrace { /// Address of the sender. pub from: H160, /// Amount of gas provided for the call. - pub gas: Gas, + #[serde(with = "super::hex_serde")] + pub gas: u64, /// Amount of gas used. - pub gas_used: Gas, + #[serde(with = "super::hex_serde")] + pub gas_used: u64, /// Address of the receiver. pub to: H160, /// Call input data. @@ -345,7 +806,7 @@ pub struct CallTrace { pub revert_reason: Option, /// List of sub-calls. #[serde(skip_serializing_if = "Vec::is_empty")] - pub calls: Vec>, + pub calls: Vec, /// List of logs emitted during the call. #[serde(skip_serializing_if = "Vec::is_empty")] pub logs: Vec, @@ -388,3 +849,41 @@ pub struct TransactionTrace { #[serde(rename = "result")] pub trace: Trace, } + +/// Serialize stack values using minimal hex format (like Geth) +fn serialize_stack_minimal(stack: &Vec, serializer: S) -> Result +where + S: serde::Serializer, +{ + let minimal_values: Vec = stack.iter().map(|bytes| bytes.to_short_hex()).collect(); + minimal_values.serialize(serializer) +} + +/// Serialize memory values without "0x" prefix (like Geth) +fn serialize_memory_no_prefix(memory: &Vec, serializer: S) -> Result +where + S: serde::Serializer, +{ + let hex_values: Vec = memory.iter().map(|bytes| bytes.to_hex_no_prefix()).collect(); + hex_values.serialize(serializer) +} + +/// Serialize storage map without "0x" prefix (like Geth) +fn serialize_storage_no_prefix( + storage: &Option>, + serializer: S, +) -> Result +where + S: serde::Serializer, +{ + match storage { + None => serializer.serialize_none(), + Some(map) => { + let mut ser_map = serializer.serialize_map(Some(map.len()))?; + for (key, value) in map { + ser_map.serialize_entry(&key.to_hex_no_prefix(), &value.to_hex_no_prefix())?; + } + ser_map.end() + }, + } +} diff --git a/substrate/frame/revive/src/evm/api/hex_serde.rs b/substrate/frame/revive/src/evm/api/hex_serde.rs index 250a38496a63f..86ca0add31943 100644 --- a/substrate/frame/revive/src/evm/api/hex_serde.rs +++ b/substrate/frame/revive/src/evm/api/hex_serde.rs @@ -41,7 +41,7 @@ macro_rules! impl_hex_codec { }; } -impl_hex_codec!(u8, u32); +impl_hex_codec!(u8, u32, u64); impl HexCodec for [u8; T] { type Error = hex::FromHexError; @@ -83,3 +83,63 @@ where let value = T::from_hex(s).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?; Ok(value) } + +pub mod option { + use super::*; + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: Serializer, + T: HexCodec, + { + match value { + Some(v) => serializer.serialize_str(&v.to_hex()), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + T: HexCodec, + ::Error: core::fmt::Debug, + { + let opt = Option::::deserialize(deserializer)?; + match opt { + Some(s) => T::from_hex(s) + .map(Some) + .map_err(|e| serde::de::Error::custom(format!("{:?}", e))), + None => Ok(None), + } + } +} + +pub mod vec { + use super::*; + use serde::ser::SerializeSeq; + + pub fn serialize(values: &Vec, serializer: S) -> Result + where + S: Serializer, + T: HexCodec, + { + let mut seq = serializer.serialize_seq(Some(values.len()))?; + for v in values { + seq.serialize_element(&v.to_hex())?; + } + seq.end() + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + T: HexCodec, + ::Error: core::fmt::Debug, + { + let strings = Vec::::deserialize(deserializer)?; + strings + .into_iter() + .map(|s| T::from_hex(s).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))) + .collect() + } +} diff --git a/substrate/frame/revive/src/evm/block_storage.rs b/substrate/frame/revive/src/evm/block_storage.rs index 0d765c7931e60..6d2f453e3f37f 100644 --- a/substrate/frame/revive/src/evm/block_storage.rs +++ b/substrate/frame/revive/src/evm/block_storage.rs @@ -88,6 +88,10 @@ impl EthereumCallResult { base_call_weight.saturating_reduce(T::WeightInfo::deposit_eth_extrinsic_revert_event()) } + crate::if_tracing(|tracer| { + tracer.dispatch_result(base_call_weight, output.weight_consumed); + }); + let result = dispatch_result(output.result, output.weight_consumed, base_call_weight); let native_fee = T::FeeInfo::compute_actual_fee(encoded_len, &info, &result); let result = T::FeeInfo::ensure_not_overdrawn(native_fee, result); diff --git a/substrate/frame/revive/src/evm/tracing.rs b/substrate/frame/revive/src/evm/tracing.rs index 0fe9dbfe076fd..eafecf1b34692 100644 --- a/substrate/frame/revive/src/evm/tracing.rs +++ b/substrate/frame/revive/src/evm/tracing.rs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. use crate::{ - evm::{CallTrace, Trace}, + evm::{CallTrace, ExecutionTrace, Trace}, tracing::Tracing, Config, }; @@ -26,6 +26,9 @@ pub use call_tracing::*; mod prestate_tracing; pub use prestate_tracing::*; +mod execution_tracing; +pub use execution_tracing::*; + /// A composite tracer. #[derive(derive_more::From, Debug)] pub enum Tracer { @@ -33,6 +36,8 @@ pub enum Tracer { CallTracer(CallTracer), /// A tracer that traces the prestate. PrestateTracer(PrestateTracer), + /// A tracer that traces opcodes and syscalls. + ExecutionTracer(ExecutionTracer), } impl Tracer @@ -44,6 +49,7 @@ where match self { Tracer::CallTracer(_) => CallTrace::default().into(), Tracer::PrestateTracer(tracer) => tracer.empty_trace().into(), + Tracer::ExecutionTracer(_) => ExecutionTrace::default().into(), } } @@ -52,6 +58,7 @@ where match self { Tracer::CallTracer(inner) => inner as &mut dyn Tracing, Tracer::PrestateTracer(inner) => inner as &mut dyn Tracing, + Tracer::ExecutionTracer(inner) => inner as &mut dyn Tracing, } } @@ -60,6 +67,12 @@ where match self { Tracer::CallTracer(inner) => inner.collect_trace().map(Trace::Call), Tracer::PrestateTracer(inner) => Some(inner.collect_trace().into()), + Tracer::ExecutionTracer(inner) => Some(inner.collect_trace().into()), } } + + /// Check if this is an execution tracer. + pub fn is_execution_tracer(&self) -> bool { + matches!(self, Tracer::ExecutionTracer(_)) + } } diff --git a/substrate/frame/revive/src/evm/tracing/call_tracing.rs b/substrate/frame/revive/src/evm/tracing/call_tracing.rs index 7452fe8d1d0d5..b492458852c52 100644 --- a/substrate/frame/revive/src/evm/tracing/call_tracing.rs +++ b/substrate/frame/revive/src/evm/tracing/call_tracing.rs @@ -27,7 +27,7 @@ use sp_core::{H160, H256, U256}; #[derive(Default, Debug, Clone, PartialEq)] pub struct CallTracer { /// Store all in-progress CallTrace instances. - traces: Vec>, + traces: Vec, /// Stack of indices to the current active traces. current_stack: Vec, /// The code and salt used to instantiate the next contract. @@ -57,7 +57,7 @@ impl Tracing for CallTracer { &mut self, contract_address: H160, beneficiary_address: H160, - gas_left: U256, + gas_left: u64, value: U256, ) { self.traces.last_mut().unwrap().calls.push(CallTrace { @@ -78,7 +78,7 @@ impl Tracing for CallTracer { is_read_only: bool, value: U256, input: &[u8], - gas_limit: U256, + gas_limit: u64, ) { // Increment parent's child call count. if let Some(&index) = self.current_stack.last() { @@ -151,7 +151,7 @@ impl Tracing for CallTracer { } } - fn exit_child_span(&mut self, output: &ExecReturnValue, gas_used: U256) { + fn exit_child_span(&mut self, output: &ExecReturnValue, gas_used: u64) { self.code_with_salt = None; // Set the output of the current trace @@ -177,7 +177,7 @@ impl Tracing for CallTracer { } } } - fn exit_child_span_with_error(&mut self, error: DispatchError, gas_used: U256) { + fn exit_child_span_with_error(&mut self, error: DispatchError, gas_used: u64) { self.code_with_salt = None; // Set the output of the current trace diff --git a/substrate/frame/revive/src/evm/tracing/execution_tracing.rs b/substrate/frame/revive/src/evm/tracing/execution_tracing.rs new file mode 100644 index 0000000000000..40b02f5f7c78d --- /dev/null +++ b/substrate/frame/revive/src/evm/tracing/execution_tracing.rs @@ -0,0 +1,301 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + evm::{ + tracing::Tracing, Bytes, ExecutionStep, ExecutionStepKind, ExecutionTrace, + ExecutionTracerConfig, + }, + tracing::{EVMFrameTraceInfo, FrameTraceInfo}, + vm::pvm::env::lookup_syscall_index, + DispatchError, ExecReturnValue, Key, Weight, +}; +use alloc::{ + collections::BTreeMap, + format, + string::{String, ToString}, + vec::Vec, +}; +use sp_core::{H160, U256}; + +/// A tracer that traces opcode and syscall execution step-by-step. +#[derive(Default, Debug, Clone, PartialEq)] +pub struct ExecutionTracer { + /// The tracer configuration. + config: ExecutionTracerConfig, + + /// The collected trace steps. + steps: Vec, + + /// Current call depth. + depth: u16, + + /// Number of steps captured (for limiting). + step_count: u64, + + /// Total gas used by the transaction. + total_gas_used: u64, + + /// The base call weight of the transaction. + base_call_weight: Weight, + + /// The Weight consumed by the transaction meter. + weight_consumed: Weight, + + /// Whether the transaction failed. + failed: bool, + + /// The return value of the transaction. + return_value: Bytes, + + /// List of storage per call + storages_per_call: Vec>, +} + +impl ExecutionTracer { + /// Create a new [`ExecutionTracer`] instance. + pub fn new(config: ExecutionTracerConfig) -> Self { + Self { + config, + steps: Vec::new(), + depth: 0, + step_count: 0, + total_gas_used: 0, + base_call_weight: Default::default(), + weight_consumed: Default::default(), + failed: false, + return_value: Bytes::default(), + storages_per_call: alloc::vec![Default::default()], + } + } + + /// Collect the traces and return them. + pub fn collect_trace(self) -> ExecutionTrace { + let Self { + steps: struct_logs, + weight_consumed, + base_call_weight, + return_value, + total_gas_used: gas, + failed, + .. + } = self; + ExecutionTrace { gas, weight_consumed, base_call_weight, failed, return_value, struct_logs } + } + + /// Record an error in the current step. + fn record_error(&mut self, error: String) { + if let Some(last_step) = self.steps.last_mut() { + last_step.error = Some(error); + } + } +} + +impl Tracing for ExecutionTracer { + fn is_execution_tracer(&self) -> bool { + true + } + + fn dispatch_result(&mut self, base_call_weight: Weight, weight_consumed: Weight) { + self.base_call_weight = base_call_weight; + self.weight_consumed = weight_consumed; + } + + fn enter_opcode(&mut self, pc: u64, opcode: u8, trace_info: &dyn EVMFrameTraceInfo) { + if self.config.limit.map(|l| self.step_count >= l).unwrap_or(false) { + return; + } + + // Extract stack data if enabled + let stack_data = + if !self.config.disable_stack { trace_info.stack_snapshot() } else { Vec::new() }; + + // Extract memory data if enabled + let memory_data = if self.config.enable_memory { + trace_info.memory_snapshot(self.config.memory_word_limit as usize) + } else { + Vec::new() + }; + + // Extract return data if enabled + let return_data = if self.config.enable_return_data { + trace_info.last_frame_output() + } else { + crate::evm::Bytes::default() + }; + + let step = ExecutionStep { + gas: trace_info.gas_left(), + gas_cost: Default::default(), + weight_cost: trace_info.weight_consumed(), + depth: self.depth, + return_data, + error: None, + kind: ExecutionStepKind::EVMOpcode { + pc: pc as u32, + op: opcode, + stack: stack_data, + memory: memory_data, + storage: None, + }, + }; + + self.steps.push(step); + self.step_count += 1; + } + + fn enter_ecall(&mut self, ecall: &'static str, args: &[u64], trace_info: &dyn FrameTraceInfo) { + if self.config.limit.map(|l| self.step_count >= l).unwrap_or(false) { + return; + } + + // Extract return data if enabled + let return_data = if self.config.enable_return_data { + trace_info.last_frame_output() + } else { + crate::evm::Bytes::default() + }; + + // Extract syscall args if enabled + let syscall_args = + if !self.config.disable_syscall_details { args.to_vec() } else { Vec::new() }; + + let step = ExecutionStep { + gas: trace_info.gas_left(), + gas_cost: Default::default(), + weight_cost: trace_info.weight_consumed(), + depth: self.depth, + return_data, + error: None, + kind: ExecutionStepKind::PVMSyscall { + op: lookup_syscall_index(ecall).unwrap_or_default(), + args: syscall_args, + returned: None, + }, + }; + + self.steps.push(step); + self.step_count += 1; + } + + fn exit_step(&mut self, trace_info: &dyn FrameTraceInfo, returned: Option) { + if let Some(step) = self.steps.last_mut() { + step.gas_cost = step.gas.saturating_sub(trace_info.gas_left()); + step.weight_cost = trace_info.weight_consumed().saturating_sub(step.weight_cost); + if !self.config.disable_syscall_details { + if let ExecutionStepKind::PVMSyscall { returned: ref mut ret, .. } = step.kind { + *ret = returned; + } + } + } + } + + fn enter_child_span( + &mut self, + _from: H160, + _to: H160, + _delegate_call: Option, + _is_read_only: bool, + _value: U256, + _input: &[u8], + _gas_limit: u64, + ) { + self.storages_per_call.push(Default::default()); + self.depth += 1; + } + + fn exit_child_span(&mut self, output: &ExecReturnValue, gas_used: u64) { + if output.did_revert() { + self.record_error("execution reverted".to_string()); + if self.depth == 0 { + self.failed = true; + } + } else { + self.return_value = Bytes(output.data.to_vec()); + } + + if self.depth == 1 { + self.total_gas_used = gas_used; + } + + self.storages_per_call.pop(); + + if self.depth > 0 { + self.depth -= 1; + } + } + + fn exit_child_span_with_error(&mut self, error: DispatchError, gas_used: u64) { + self.record_error(format!("{:?}", error)); + + // Mark as failed if this is the top-level call + if self.depth == 1 { + self.failed = true; + self.total_gas_used = gas_used; + } + + if self.depth > 0 { + self.depth -= 1; + } + + self.storages_per_call.pop(); + } + + fn storage_write(&mut self, key: &Key, _old_value: Option>, new_value: Option<&[u8]>) { + // Only track storage if not disabled + if self.config.disable_storage { + return; + } + + if let Some(storage) = self.storages_per_call.last_mut() { + let key_bytes = crate::evm::Bytes(key.unhashed().to_vec()); + let value_bytes = crate::evm::Bytes( + new_value.map(|v| v.to_vec()).unwrap_or_else(|| alloc::vec![0u8; 32]), + ); + storage.insert(key_bytes, value_bytes); + + if let Some(step) = self.steps.last_mut() { + if let ExecutionStepKind::EVMOpcode { storage: ref mut step_storage, .. } = + step.kind + { + *step_storage = Some(storage.clone()); + } + } + } + } + + fn storage_read(&mut self, key: &Key, value: Option<&[u8]>) { + // Only track storage if not disabled + if self.config.disable_storage { + return; + } + + if let Some(storage) = self.storages_per_call.last_mut() { + let key_bytes = crate::evm::Bytes(key.unhashed().to_vec()); + storage.entry(key_bytes).or_insert_with(|| { + crate::evm::Bytes(value.map(|v| v.to_vec()).unwrap_or_else(|| alloc::vec![0u8; 32])) + }); + + if let Some(step) = self.steps.last_mut() { + if let ExecutionStepKind::EVMOpcode { storage: ref mut step_storage, .. } = + step.kind + { + *step_storage = Some(storage.clone()); + } + } + } + } +} diff --git a/substrate/frame/revive/src/evm/tracing/prestate_tracing.rs b/substrate/frame/revive/src/evm/tracing/prestate_tracing.rs index b1141924d6e12..82785e58c4fa9 100644 --- a/substrate/frame/revive/src/evm/tracing/prestate_tracing.rs +++ b/substrate/frame/revive/src/evm/tracing/prestate_tracing.rs @@ -241,7 +241,7 @@ where &mut self, contract_address: H160, beneficiary_address: H160, - _gas_left: U256, + _gas_left: u64, _value: U256, ) { self.destructed_addrs.insert(contract_address); @@ -258,7 +258,7 @@ where _is_read_only: bool, _value: U256, _input: &[u8], - _gas_limit: U256, + _gas_limit: u64, ) { if let Some(delegate_call) = delegate_call { self.calls.push(self.current_addr()); @@ -275,11 +275,11 @@ where } } - fn exit_child_span_with_error(&mut self, _error: crate::DispatchError, _gas_used: U256) { + fn exit_child_span_with_error(&mut self, _error: crate::DispatchError, _gas_used: u64) { self.calls.pop(); } - fn exit_child_span(&mut self, output: &ExecReturnValue, _gas_used: U256) { + fn exit_child_span(&mut self, output: &ExecReturnValue, _gas_used: u64) { let current_addr = self.calls.pop().unwrap_or_default(); if output.did_revert() { return diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index 676b1b34eeeb4..f4ec66eb3bc17 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -1239,7 +1239,12 @@ where frame.read_only, frame.value_transferred, &input_data, - frame.frame_meter.eth_gas_left().unwrap_or_default().into(), + frame + .frame_meter + .eth_gas_left() + .unwrap_or_default() + .try_into() + .unwrap_or_default(), ); }); let mock_answer = self.exec_config.mock_handler.as_ref().and_then(|handler| { @@ -1458,11 +1463,13 @@ where // we treat the initial frame meter differently to address // https://github.com/paritytech/polkadot-sdk/issues/8362 let gas_consumed = if is_first_frame { - frame_meter.total_consumed_gas().into() + frame_meter.total_consumed_gas() } else { - frame_meter.eth_gas_consumed().into() + frame_meter.eth_gas_consumed() }; + let gas_consumed: u64 = gas_consumed.try_into().unwrap_or(u64::MAX); + match &output { Ok(output) => tracer.exit_child_span(&output, gas_consumed), Err(e) => tracer.exit_child_span_with_error(e.error.into(), gas_consumed), @@ -1480,11 +1487,12 @@ where // we treat the initial frame meter differently to address // https://github.com/paritytech/polkadot-sdk/issues/8362 let gas_consumed = if is_first_frame { - frame_meter.total_consumed_gas().into() + frame_meter.total_consumed_gas() } else { - frame_meter.eth_gas_consumed().into() + frame_meter.eth_gas_consumed() }; + let gas_consumed: u64 = gas_consumed.try_into().unwrap_or(u64::MAX); tracer.exit_child_span_with_error(error.into(), gas_consumed); }); @@ -1884,7 +1892,12 @@ where tracer.terminate( addr, *beneficiary, - self.top_frame().frame_meter.eth_gas_left().unwrap_or_default().into(), + self.top_frame() + .frame_meter + .eth_gas_left() + .unwrap_or_default() + .try_into() + .unwrap_or_default(), crate::Pallet::::evm_balance(&addr), ); }); diff --git a/substrate/frame/revive/src/lib.rs b/substrate/frame/revive/src/lib.rs index cdec816cd9d5c..581b8f6928e3d 100644 --- a/substrate/frame/revive/src/lib.rs +++ b/substrate/frame/revive/src/lib.rs @@ -49,8 +49,8 @@ pub mod weights; use crate::{ evm::{ block_hash::EthereumBlockBuilderIR, block_storage, fees::InfoT as FeeInfo, - runtime::SetWeightLimit, CallTracer, CreateCallMode, GenericTransaction, PrestateTracer, - Trace, Tracer, TracerType, TYPE_EIP1559, + runtime::SetWeightLimit, CallTracer, CreateCallMode, ExecutionTracer, GenericTransaction, + PrestateTracer, Trace, Tracer, TracerType, TYPE_EIP1559, }, exec::{AccountIdOf, ExecError, ReentrancyProtection, Stack as ExecStack}, storage::{AccountType, DeletionQueueManager}, @@ -734,7 +734,7 @@ pub mod pallet { #[derive(Clone, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] pub struct ContractData { /// Contract code. - pub code: Vec, + pub code: crate::evm::Bytes, /// Initial storage entries as 32-byte key/value pairs. pub storage: alloc::collections::BTreeMap, } @@ -813,12 +813,12 @@ pub mod pallet { ); }, Some(genesis::ContractData { code, storage }) => { - let blob = if code.starts_with(&polkavm_common::program::BLOB_MAGIC) { - ContractBlob::::from_pvm_code( code.clone(), owner.clone()).inspect_err(|err| { + let blob = if code.0.starts_with(&polkavm_common::program::BLOB_MAGIC) { + ContractBlob::::from_pvm_code( code.0.clone(), owner.clone()).inspect_err(|err| { log::error!(target: LOG_TARGET, "Failed to create PVM ContractBlob for {address:?}: {err:?}"); }) } else { - ContractBlob::::from_evm_runtime_code(code.clone(), account_id).inspect_err(|err| { + ContractBlob::::from_evm_runtime_code(code.0.clone(), account_id).inspect_err(|err| { log::error!(target: LOG_TARGET, "Failed to create EVM ContractBlob for {address:?}: {err:?}"); }) }; @@ -841,7 +841,7 @@ pub mod pallet { AccountInfo { account_type: info.clone().into(), dust: 0 }, ); - >::insert(blob.code_hash(), code); + >::insert(blob.code_hash(), code.0.clone()); >::insert(blob.code_hash(), blob.code_info().clone()); for (k, v) in storage { let _ = info.write(&Key::from_fixed(k.0), Some(v.0.to_vec()), None, false).inspect_err(|err| { @@ -2137,6 +2137,8 @@ impl Pallet { TracerType::CallTracer(config) => CallTracer::new(config.unwrap_or_default()).into(), TracerType::PrestateTracer(config) => PrestateTracer::new(config.unwrap_or_default()).into(), + TracerType::ExecutionTracer(config) => + ExecutionTracer::new(config.unwrap_or_default()).into(), } } @@ -2879,6 +2881,13 @@ macro_rules! impl_runtime_apis_plus_revive_traits { tracer_type: $crate::evm::TracerType, ) -> Vec<(u32, $crate::evm::Trace)> { use $crate::{sp_runtime::traits::Block, tracing::trace}; + + if matches!(tracer_type, $crate::evm::TracerType::ExecutionTracer(_)) && + !$crate::DebugSettings::is_execution_tracing_enabled::() + { + return Default::default() + } + let mut traces = vec![]; let (header, extrinsics) = block.deconstruct(); <$Executive>::initialize_block(&header); @@ -2902,6 +2911,12 @@ macro_rules! impl_runtime_apis_plus_revive_traits { ) -> Option<$crate::evm::Trace> { use $crate::{sp_runtime::traits::Block, tracing::trace}; + if matches!(tracer_type, $crate::evm::TracerType::ExecutionTracer(_)) && + !$crate::DebugSettings::is_execution_tracing_enabled::() + { + return None + } + let mut tracer = $crate::Pallet::::evm_tracer(tracer_type); let (header, extrinsics) = block.deconstruct(); @@ -2924,6 +2939,13 @@ macro_rules! impl_runtime_apis_plus_revive_traits { tracer_type: $crate::evm::TracerType, ) -> Result<$crate::evm::Trace, $crate::EthTransactError> { use $crate::tracing::trace; + + if matches!(tracer_type, $crate::evm::TracerType::ExecutionTracer(_)) && + !$crate::DebugSettings::is_execution_tracing_enabled::() + { + return Err($crate::EthTransactError::Message("Execution Tracing is disabled".into())) + } + let mut tracer = $crate::Pallet::::evm_tracer(tracer_type.clone()); let t = tracer.as_tracing(); diff --git a/substrate/frame/revive/src/tests.rs b/substrate/frame/revive/src/tests.rs index 9785e526f91a8..614a8a25e8978 100644 --- a/substrate/frame/revive/src/tests.rs +++ b/substrate/frame/revive/src/tests.rs @@ -575,7 +575,7 @@ fn ext_builder_with_genesis_config_works() { balance: U256::from(100_000_100), nonce: 42, contract_data: Some(ContractData { - code: compile_module("dummy").unwrap().0, + code: compile_module("dummy").unwrap().0.into(), storage: [([1u8; 32].into(), [2u8; 32].into())].into_iter().collect(), }), }; @@ -591,7 +591,8 @@ fn ext_builder_with_genesis_config_works() { revm::bytecode::opcode::PUSH1, 0x00, revm::bytecode::opcode::RETURN, - ], + ] + .into(), storage: [([3u8; 32].into(), [4u8; 32].into())].into_iter().collect(), }), }; @@ -627,7 +628,7 @@ fn ext_builder_with_genesis_config_works() { assert_eq!( PristineCode::::get(&contract_info.code_hash).unwrap(), - contract_data.code + contract_data.code.0 ); assert_eq!(Pallet::::evm_nonce(&contract.address), contract.nonce); assert_eq!(Pallet::::evm_balance(&contract.address), contract.balance); diff --git a/substrate/frame/revive/src/tests/pvm.rs b/substrate/frame/revive/src/tests/pvm.rs index bc02293d8b055..9fb379c427426 100644 --- a/substrate/frame/revive/src/tests/pvm.rs +++ b/substrate/frame/revive/src/tests/pvm.rs @@ -3927,7 +3927,7 @@ fn call_tracing_works() { a.gas_consumed }); let gas_trace = tracer.collect_trace().unwrap(); - assert_eq!(&gas_trace.gas_used, &gas_used.into()); + assert_eq!(&gas_trace.gas_used, &gas_used); for config in tracer_configs { let logs = if config.with_logs { @@ -3964,8 +3964,8 @@ fn call_tracing_works() { error: Some("execution reverted".to_string()), call_type: Call, value: Some(U256::from(0)), - gas: 0.into(), - gas_used: 0.into(), + gas: 0, + gas_used: 0, ..Default::default() }, CallTrace { @@ -3975,8 +3975,8 @@ fn call_tracing_works() { call_type: Call, logs: logs.clone(), value: Some(U256::from(0)), - gas: 0.into(), - gas_used: 0.into(), + gas: 0, + gas_used: 0, calls: vec![ CallTrace { from: addr, @@ -3986,8 +3986,8 @@ fn call_tracing_works() { error: Some("ContractTrapped".to_string()), call_type: Call, value: Some(U256::from(0)), - gas: 0.into(), - gas_used: 0.into(), + gas: 0, + gas_used: 0, ..Default::default() }, CallTrace { @@ -3997,8 +3997,8 @@ fn call_tracing_works() { call_type: Call, logs: logs.clone(), value: Some(U256::from(0)), - gas: 0.into(), - gas_used: 0.into(), + gas: 0, + gas_used: 0, calls: vec![ CallTrace { from: addr, @@ -4007,8 +4007,8 @@ fn call_tracing_works() { output: 0u32.to_le_bytes().to_vec().into(), call_type: Call, value: Some(U256::from(0)), - gas: 0.into(), - gas_used: 0.into(), + gas: 0, + gas_used: 0, ..Default::default() }, CallTrace { @@ -4017,8 +4017,8 @@ fn call_tracing_works() { input: (0u32, addr_callee).encode().into(), call_type: Call, value: Some(U256::from(0)), - gas: 0.into(), - gas_used: 0.into(), + gas: 0, + gas_used: 0, calls: vec![ CallTrace { from: addr, @@ -4057,8 +4057,8 @@ fn call_tracing_works() { value: Some(U256::from(0)), calls: calls, child_call_count: 2, - gas: 0.into(), - gas_used: 0.into(), + gas: 0, + gas_used: 0, ..Default::default() }; @@ -4937,7 +4937,7 @@ fn eip3607_allow_tx_from_contract_or_precompile_if_debug_setting_configured() { let (binary, code_hash) = compile_module("dummy").unwrap(); let genesis_config = GenesisConfig:: { - debug_settings: Some(DebugSettings::new(false, true, false)), + debug_settings: Some(DebugSettings::default().set_bypass_eip_3607(true)), ..Default::default() }; @@ -5208,12 +5208,12 @@ fn self_destruct_by_syscall_tracing_works() { to: addr, call_type: CallType::Call, value: Some(U256::zero()), - gas: 0.into(), - gas_used: 0.into(), + gas: 0, + gas_used: 0, calls: vec![CallTrace { from: addr, to: DJANGO_ADDR, - gas: 0.into(), + gas: 0, call_type: CallType::Selfdestruct, value: Some(Pallet::::convert_native_to_evm(100_000u64)), @@ -5224,9 +5224,9 @@ fn self_destruct_by_syscall_tracing_works() { }), modify_trace_fn: Some(Box::new(|mut actual_trace| { if let Trace::Call(trace) = &mut actual_trace { - trace.gas = 0.into(); - trace.gas_used = 0.into(); - trace.calls[0].gas = 0.into(); + trace.gas = 0; + trace.gas_used = 0; + trace.calls[0].gas = 0; } actual_trace })), @@ -5365,6 +5365,7 @@ fn self_destruct_by_syscall_tracing_works() { let trace_wrapped = match trace { crate::evm::Trace::Call(ct) => Trace::Call(ct), crate::evm::Trace::Prestate(pt) => Trace::Prestate(pt), + crate::evm::Trace::Execution(_) => panic!("Execution trace not expected"), }; assert_eq!(trace_wrapped, expected_trace, "Trace mismatch for: {}", description); diff --git a/substrate/frame/revive/src/tests/sol.rs b/substrate/frame/revive/src/tests/sol.rs index 919b3f8dfe3e7..3eeece5d4b203 100644 --- a/substrate/frame/revive/src/tests/sol.rs +++ b/substrate/frame/revive/src/tests/sol.rs @@ -190,7 +190,10 @@ fn eth_contract_too_large() { // Initialize genesis config with allow_unlimited_contract_size let genesis_config = GenesisConfig:: { - debug_settings: Some(DebugSettings::new(allow_unlimited_contract_size, false, false)), + debug_settings: Some( + DebugSettings::default() + .set_allow_unlimited_contract_size(allow_unlimited_contract_size), + ), ..Default::default() }; @@ -593,3 +596,255 @@ fn eth_substrate_call_tracks_weight_correctly() { ); }); } + +#[test] +fn execution_tracing_works_for_evm() { + use crate::{ + evm::{ + ExecutionStep, ExecutionStepKind, ExecutionTrace, ExecutionTracer, + ExecutionTracerConfig, + }, + tracing::trace, + }; + use sp_core::U256; + let (code, _) = compile_module_with_type("Fibonacci", FixtureType::Solc).unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000); + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let config = ExecutionTracerConfig { + enable_memory: false, + disable_stack: false, + disable_storage: true, + enable_return_data: true, + disable_syscall_details: true, + limit: Some(5), + memory_word_limit: 16, + }; + + let mut tracer = ExecutionTracer::new(config); + let _result = trace(&mut tracer, || { + builder::bare_call(addr) + .data(Fibonacci::FibonacciCalls::fib(Fibonacci::fibCall { n: 3u64 }).abi_encode()) + .build_and_unwrap_result() + }); + + let mut actual_trace = tracer.collect_trace(); + actual_trace.struct_logs.iter_mut().for_each(|step| { + step.gas = Default::default(); + step.gas_cost = Default::default(); + step.weight_cost = Default::default(); + }); + + let expected_trace = ExecutionTrace { + gas: actual_trace.gas, + base_call_weight: Default::default(), + weight_consumed: Default::default(), + failed: false, + return_value: crate::evm::Bytes(U256::from(2).to_big_endian().to_vec()), + struct_logs: vec![ + ExecutionStep { + depth: 1, + return_data: crate::evm::Bytes::default(), + gas: Default::default(), + gas_cost: Default::default(), + weight_cost: Default::default(), + error: None, + kind: ExecutionStepKind::EVMOpcode { + pc: 0, + op: PUSH1, + stack: vec![], + memory: vec![], + storage: None, + }, + }, + ExecutionStep { + depth: 1, + return_data: crate::evm::Bytes::default(), + error: None, + gas: Default::default(), + gas_cost: Default::default(), + weight_cost: Default::default(), + kind: ExecutionStepKind::EVMOpcode { + pc: 2, + op: PUSH1, + stack: vec![crate::evm::Bytes(U256::from(0x80).to_big_endian().to_vec())], + memory: vec![], + storage: None, + }, + }, + ExecutionStep { + depth: 1, + return_data: crate::evm::Bytes::default(), + error: None, + gas: Default::default(), + gas_cost: Default::default(), + weight_cost: Default::default(), + kind: ExecutionStepKind::EVMOpcode { + pc: 4, + op: MSTORE, + stack: vec![ + crate::evm::Bytes(U256::from(0x80).to_big_endian().to_vec()), + crate::evm::Bytes(U256::from(0x40).to_big_endian().to_vec()), + ], + memory: vec![], + storage: None, + }, + }, + ExecutionStep { + depth: 1, + return_data: crate::evm::Bytes::default(), + error: None, + gas: Default::default(), + gas_cost: Default::default(), + weight_cost: Default::default(), + kind: ExecutionStepKind::EVMOpcode { + pc: 5, + op: CALLVALUE, + stack: vec![], + memory: vec![], + storage: None, + }, + }, + ExecutionStep { + depth: 1, + return_data: crate::evm::Bytes::default(), + error: None, + gas: Default::default(), + gas_cost: Default::default(), + weight_cost: Default::default(), + kind: ExecutionStepKind::EVMOpcode { + pc: 6, + op: DUP1, + stack: vec![crate::evm::Bytes(U256::from(0).to_big_endian().to_vec())], + memory: vec![], + storage: None, + }, + }, + ], + }; + + assert_eq!(actual_trace, expected_trace); + }); +} + +#[test] +fn execution_tracing_works_for_pvm() { + use crate::{ + evm::{ + ExecutionStep, ExecutionStepKind, ExecutionTrace, ExecutionTracer, + ExecutionTracerConfig, + }, + tracing::trace, + vm::pvm::env::lookup_syscall_index, + }; + use sp_core::U256; + let (code, _) = compile_module_with_type("Fibonacci", FixtureType::Resolc).unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000); + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let config = ExecutionTracerConfig { + enable_return_data: true, + limit: Some(5), + ..Default::default() + }; + + let mut tracer = ExecutionTracer::new(config); + let _result = trace(&mut tracer, || { + builder::bare_call(addr) + .data(Fibonacci::FibonacciCalls::fib(Fibonacci::fibCall { n: 3u64 }).abi_encode()) + .build_and_unwrap_result() + }); + + let mut actual_trace = tracer.collect_trace(); + actual_trace.struct_logs.iter_mut().for_each(|step| { + step.gas = Default::default(); + step.gas_cost = Default::default(); + step.weight_cost = Default::default(); + // Replace args with 42 as they contain memory addresses that may vary between runs + if let ExecutionStepKind::PVMSyscall { args, .. } = &mut step.kind { + args.iter_mut().for_each(|arg| *arg = 42); + } + }); + + let expected_trace = ExecutionTrace { + gas: actual_trace.gas, + base_call_weight: Default::default(), + weight_consumed: Default::default(), + failed: false, + return_value: crate::evm::Bytes(U256::from(2).to_big_endian().to_vec()), + struct_logs: vec![ + ExecutionStep { + depth: 1, + return_data: crate::evm::Bytes::default(), + error: None, + gas: Default::default(), + gas_cost: Default::default(), + weight_cost: Default::default(), + kind: ExecutionStepKind::PVMSyscall { + op: lookup_syscall_index("call_data_size").unwrap_or_default(), + args: vec![], + returned: Some(36), + }, + }, + ExecutionStep { + depth: 1, + return_data: crate::evm::Bytes::default(), + error: None, + gas: Default::default(), + gas_cost: Default::default(), + weight_cost: Default::default(), + kind: ExecutionStepKind::PVMSyscall { + op: lookup_syscall_index("call_data_load").unwrap_or_default(), + args: vec![42, 42], + returned: None, + }, + }, + ExecutionStep { + depth: 1, + return_data: crate::evm::Bytes::default(), + error: None, + gas: Default::default(), + gas_cost: Default::default(), + weight_cost: Default::default(), + kind: ExecutionStepKind::PVMSyscall { + op: lookup_syscall_index("value_transferred").unwrap_or_default(), + args: vec![42], + returned: None, + }, + }, + ExecutionStep { + depth: 1, + return_data: crate::evm::Bytes::default(), + error: None, + gas: Default::default(), + gas_cost: Default::default(), + weight_cost: Default::default(), + kind: ExecutionStepKind::PVMSyscall { + op: lookup_syscall_index("call_data_load").unwrap_or_default(), + args: vec![42, 42], + returned: None, + }, + }, + ExecutionStep { + depth: 1, + return_data: crate::evm::Bytes::default(), + error: None, + gas: Default::default(), + gas_cost: Default::default(), + weight_cost: Default::default(), + kind: ExecutionStepKind::PVMSyscall { + op: lookup_syscall_index("seal_return").unwrap_or_default(), + args: vec![42, 42, 42], + returned: None, + }, + }, + ], + }; + + assert_eq!(actual_trace, expected_trace); + }); +} diff --git a/substrate/frame/revive/src/tracing.rs b/substrate/frame/revive/src/tracing.rs index 7da708829df90..10e35fde24e67 100644 --- a/substrate/frame/revive/src/tracing.rs +++ b/substrate/frame/revive/src/tracing.rs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{primitives::ExecReturnValue, Code, DispatchError, Key}; +use crate::{evm::Bytes, primitives::ExecReturnValue, Code, DispatchError, Key, Weight}; use alloc::vec::Vec; use environmental::environmental; use sp_core::{H160, H256, U256}; @@ -40,6 +40,30 @@ pub(crate) fn if_tracing R>(f: F) tracer::with(f) } +/// Interface to provide frame trace information for the current execution frame. +pub trait FrameTraceInfo { + /// Get the amount of gas remaining in the current frame. + fn gas_left(&self) -> u64; + + /// Returns how much weight was spent + fn weight_consumed(&self) -> Weight; + + /// Get the output from the last frame. + fn last_frame_output(&self) -> Bytes; +} + +/// Interface to provide EVM-specific trace information for the current execution frame. +pub trait EVMFrameTraceInfo: FrameTraceInfo { + /// Get a snapshot of the memory at this point in execution. + /// + /// # Parameters + /// - `limit`: Maximum number of memory words to capture. + fn memory_snapshot(&self, limit: usize) -> Vec; + + /// Get a snapshot of the stack at this point in execution. + fn stack_snapshot(&self) -> Vec; +} + /// Defines methods to trace contract interactions. pub trait Tracing { /// Register an address that should be traced. @@ -54,7 +78,7 @@ pub trait Tracing { _is_read_only: bool, _value: U256, _input: &[u8], - _gas_limit: U256, + _gas_limit: u64, ) { } @@ -63,7 +87,7 @@ pub trait Tracing { &mut self, _contract_address: H160, _beneficiary_address: H160, - _gas_left: U256, + _gas_left: u64, _value: U256, ) { } @@ -90,8 +114,51 @@ pub trait Tracing { fn log_event(&mut self, _event: H160, _topics: &[H256], _data: &[u8]) {} /// Called after a contract call is executed - fn exit_child_span(&mut self, _output: &ExecReturnValue, _gas_used: U256) {} + fn exit_child_span(&mut self, _output: &ExecReturnValue, _gas_used: u64) {} /// Called when a contract call terminates with an error - fn exit_child_span_with_error(&mut self, _error: DispatchError, _gas_used: U256) {} + fn exit_child_span_with_error(&mut self, _error: DispatchError, _gas_used: u64) {} + + /// Check if the tracer is an execution tracer. + fn is_execution_tracer(&self) -> bool { + false + } + + /// Called before an EVM opcode is executed. + /// + /// # Parameters + /// - `pc`: The current program counter. + /// - `opcode`: The opcode being executed. + /// - `trace_info`: Information about the current execution frame. + fn enter_opcode(&mut self, _pc: u64, _opcode: u8, _trace_info: &dyn EVMFrameTraceInfo) {} + + /// Called before a PVM syscall is executed. + /// + /// # Parameters + /// - `ecall`: The name of the syscall being executed. + /// - `args`: The syscall arguments (register values). + /// - `trace_info`: Information about the current execution frame. + fn enter_ecall( + &mut self, + _ecall: &'static str, + _args: &[u64], + _trace_info: &dyn FrameTraceInfo, + ) { + } + + /// Called after an EVM opcode or PVM syscall is executed to record the gas cost. + /// + /// # Parameters + /// - `trace_info`: Information about the current execution frame. + /// - `returned`: The syscall return value (PVM only, `None` for EVM opcodes). + fn exit_step(&mut self, _trace_info: &dyn FrameTraceInfo, _returned: Option) {} + + /// Called once the transaction completes to report the gas consumed by the meter. + /// + /// # Parameters + /// - `base_call_weight`: Extrinsic base weight that is added on top of `weight_consumed` when + /// charging. + /// - `weight_consumed`: Weight used by the transaction logic, excluding the extrinsic base + /// weight. + fn dispatch_result(&mut self, _base_call_weight: Weight, _weight_consumed: Weight) {} } diff --git a/substrate/frame/revive/src/vm/evm.rs b/substrate/frame/revive/src/vm/evm.rs index 8d60d14d23b45..643c7e2827b98 100644 --- a/substrate/frame/revive/src/vm/evm.rs +++ b/substrate/frame/revive/src/vm/evm.rs @@ -17,6 +17,7 @@ use crate::{ debug::DebugSettings, precompiles::Token, + tracing, vm::{evm::instructions::exec_instruction, BytecodeType, ExecResult, Ext}, weights::WeightInfo, AccountIdOf, CodeInfo, Config, ContractBlob, DispatchError, Error, Weight, H256, LOG_TARGET, @@ -129,7 +130,13 @@ impl ContractBlob { /// Calls the EVM interpreter with the provided bytecode and inputs. pub fn call(bytecode: Bytecode, ext: &mut E, input: Vec) -> ExecResult { let mut interpreter = Interpreter::new(ExtBytecode::new(bytecode), input, ext); - let ControlFlow::Break(halt) = run_plain(&mut interpreter); + let tracing_enabled = tracing::if_tracing(|t| t.is_execution_tracer()).unwrap_or(false); + + let ControlFlow::Break(halt) = if tracing_enabled { + run_plain_with_tracing(&mut interpreter) + } else { + run_plain(&mut interpreter) + }; halt.into() } @@ -140,3 +147,22 @@ fn run_plain(interpreter: &mut Interpreter) -> ControlFlow( + interpreter: &mut Interpreter, +) -> ControlFlow { + loop { + let opcode = interpreter.bytecode.opcode(); + tracing::if_tracing(|tracer| { + let pc = interpreter.bytecode.pc() as u64; + tracer.enter_opcode(pc, opcode, interpreter) + }); + + interpreter.bytecode.relative_jump(1); + let res = exec_instruction(interpreter, opcode); + + tracing::if_tracing(|tracer| tracer.exit_step(interpreter, None)); + + res?; + } +} diff --git a/substrate/frame/revive/src/vm/evm/interpreter.rs b/substrate/frame/revive/src/vm/evm/interpreter.rs index be5663499ba6e..97fd16a87b1d3 100644 --- a/substrate/frame/revive/src/vm/evm/interpreter.rs +++ b/substrate/frame/revive/src/vm/evm/interpreter.rs @@ -18,11 +18,12 @@ use super::ExtBytecode; use crate::{ primitives::ExecReturnValue, + tracing::FrameTraceInfo, vm::{ evm::{memory::Memory, stack::Stack}, ExecResult, Ext, }, - Config, DispatchError, Error, + Config, DispatchError, Error, Weight, }; use alloc::vec::Vec; use pallet_revive_uapi::ReturnFlags; @@ -74,3 +75,29 @@ impl<'a, E: Ext> Interpreter<'a, E> { Self { ext, bytecode, input, stack: Stack::new(), memory: Memory::new() } } } + +impl FrameTraceInfo for Interpreter<'_, E> { + fn gas_left(&self) -> u64 { + let meter = self.ext.frame_meter(); + meter.eth_gas_left().unwrap_or_default().try_into().unwrap_or_default() + } + + fn weight_consumed(&self) -> Weight { + let meter = self.ext.frame_meter(); + meter.weight_consumed() + } + + fn last_frame_output(&self) -> crate::evm::Bytes { + crate::evm::Bytes(self.ext.last_frame_output().data.clone()) + } +} + +impl crate::tracing::EVMFrameTraceInfo for Interpreter<'_, E> { + fn memory_snapshot(&self, limit: usize) -> Vec { + self.memory.snapshot(limit) + } + + fn stack_snapshot(&self) -> Vec { + self.stack.snapshot() + } +} diff --git a/substrate/frame/revive/src/vm/evm/memory.rs b/substrate/frame/revive/src/vm/evm/memory.rs index 15e9d0f46aa14..035fd5eb5439b 100644 --- a/substrate/frame/revive/src/vm/evm/memory.rs +++ b/substrate/frame/revive/src/vm/evm/memory.rs @@ -53,6 +53,10 @@ impl Memory { &mut self.data[offset..offset + len] } + fn get_word(&self, offset: usize) -> &[u8; 32] { + self.data[offset..offset + 32].try_into().unwrap() + } + /// Get the current memory size in bytes pub fn size(&self) -> usize { self.data.len() @@ -126,6 +130,20 @@ impl Memory { pub fn copy(&mut self, dst: usize, src: usize, len: usize) { self.data.copy_within(src..src + len, dst); } + + /// Returns a snapshot of the memory in 32-byte chunks, limited to the specified number of + /// chunks. + pub fn snapshot(&self, limit: usize) -> Vec { + let mut memory_bytes = Vec::new(); + let words_to_read = core::cmp::min(self.size().div_ceil(32), limit); + + for i in 0..words_to_read { + let word = self.get_word(i * 32); + memory_bytes.push(crate::evm::Bytes(word.to_vec())); + } + + memory_bytes + } } #[cfg(test)] diff --git a/substrate/frame/revive/src/vm/evm/stack.rs b/substrate/frame/revive/src/vm/evm/stack.rs index 7219ac81ccf15..7ab47ded45527 100644 --- a/substrate/frame/revive/src/vm/evm/stack.rs +++ b/substrate/frame/revive/src/vm/evm/stack.rs @@ -175,6 +175,17 @@ impl Stack { self.stack.push(U256::from_big_endian(&word_bytes)); return ControlFlow::Continue(()); } + + /// Returns a snapshot of the stack as bytes. + pub fn snapshot(&self) -> Vec { + let mut stack_bytes = Vec::new(); + for value in self.stack.iter() { + let bytes = value.to_big_endian().to_vec(); + stack_bytes.push(crate::evm::Bytes(bytes)); + } + + stack_bytes + } } #[cfg(test)] diff --git a/substrate/frame/revive/src/vm/pvm.rs b/substrate/frame/revive/src/vm/pvm.rs index 6fb4cbca9aa70..ba7cf44aa2d35 100644 --- a/substrate/frame/revive/src/vm/pvm.rs +++ b/substrate/frame/revive/src/vm/pvm.rs @@ -19,20 +19,20 @@ pub mod env; -#[cfg(doc)] -pub use env::SyscallDoc; - use crate::{ exec::{CallResources, ExecError, ExecResult, Ext, Key}, limits, metering::ChargedAmount, precompiles::{All as AllPrecompiles, Precompiles}, primitives::ExecReturnValue, + tracing::FrameTraceInfo, Code, Config, Error, Pallet, ReentrancyProtection, RuntimeCosts, LOG_TARGET, SENTINEL, }; use alloc::{vec, vec::Vec}; use codec::Encode; use core::{fmt, marker::PhantomData, mem}; +#[cfg(doc)] +pub use env::SyscallDoc; use frame_support::{ensure, weights::Weight}; use pallet_revive_uapi::{CallFlags, ReturnErrorCode, ReturnFlags, StorageFlags}; use sp_core::{H160, H256, U256}; @@ -814,6 +814,21 @@ impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { } } +impl<'a, E: Ext, M: ?Sized + Memory> FrameTraceInfo for Runtime<'a, E, M> { + fn gas_left(&self) -> u64 { + let meter = self.ext.frame_meter(); + meter.eth_gas_left().unwrap_or_default().try_into().unwrap_or_default() + } + fn weight_consumed(&self) -> Weight { + let meter = self.ext.frame_meter(); + meter.weight_consumed() + } + + fn last_frame_output(&self) -> crate::evm::Bytes { + crate::evm::Bytes(self.ext.last_frame_output().data.clone()) + } +} + pub struct PreparedCall<'a, E: Ext> { module: polkavm::Module, instance: polkavm::RawInstance, diff --git a/substrate/frame/revive/uapi/src/lib.rs b/substrate/frame/revive/uapi/src/lib.rs index 5e3cb0ec5e705..6537f3f0b675c 100644 --- a/substrate/frame/revive/uapi/src/lib.rs +++ b/substrate/frame/revive/uapi/src/lib.rs @@ -55,7 +55,7 @@ macro_rules! define_error_codes { )* ) => { /// Every error that can be returned to a contract when it calls any of the host functions. - #[derive(Debug, PartialEq, Eq)] + #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[repr(u32)] pub enum ReturnErrorCode { /// API call successful.