diff --git a/Cargo.lock b/Cargo.lock index 2bae43979562..7cf9340f4fdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15162,6 +15162,7 @@ dependencies = [ "paste", "polkavm-derive 0.18.0", "scale-info", + "serde", ] [[package]] diff --git a/substrate/frame/revive/Cargo.toml b/substrate/frame/revive/Cargo.toml index 5d2bfb4f795e..5ba1ee4b2af1 100644 --- a/substrate/frame/revive/Cargo.toml +++ b/substrate/frame/revive/Cargo.toml @@ -38,7 +38,7 @@ frame-benchmarking = { optional = true, workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } pallet-revive-fixtures = { workspace = true, optional = true } -pallet-revive-uapi = { workspace = true, features = ["scale"] } +pallet-revive-uapi = { workspace = true, features = ["scale", "serde"] } pallet-revive-proc-macro = { workspace = true } pallet-transaction-payment = { workspace = true } sp-api = { workspace = true } diff --git a/substrate/frame/revive/rpc/examples/js/contracts/Tracing.sol b/substrate/frame/revive/rpc/examples/js/contracts/Tracing.sol index 9eb5469e010d..8aa1dde90a3f 100644 --- a/substrate/frame/revive/rpc/examples/js/contracts/Tracing.sol +++ b/substrate/frame/revive/rpc/examples/js/contracts/Tracing.sol @@ -11,7 +11,10 @@ contract TracingCaller { function start(uint256 counter) external { if (counter == 0) { - return; + uint256 a = 1; + uint256 b = 0; + uint256 c = a / b; + return; } TracingCallee(callee).consumeGas(); diff --git a/substrate/frame/revive/rpc/examples/js/pvm/ErrorTester.polkavm b/substrate/frame/revive/rpc/examples/js/pvm/ErrorTester.polkavm index 77de4ff3b1b3..f8e8f00034f4 100644 Binary files a/substrate/frame/revive/rpc/examples/js/pvm/ErrorTester.polkavm and b/substrate/frame/revive/rpc/examples/js/pvm/ErrorTester.polkavm differ diff --git a/substrate/frame/revive/rpc/examples/js/pvm/EventExample.polkavm b/substrate/frame/revive/rpc/examples/js/pvm/EventExample.polkavm index 6dbc5ca8b108..3814f07712d1 100644 Binary files a/substrate/frame/revive/rpc/examples/js/pvm/EventExample.polkavm and b/substrate/frame/revive/rpc/examples/js/pvm/EventExample.polkavm differ diff --git a/substrate/frame/revive/rpc/examples/js/pvm/Flipper.polkavm b/substrate/frame/revive/rpc/examples/js/pvm/Flipper.polkavm index 488ee684f0c4..bcfe5e217c94 100644 Binary files a/substrate/frame/revive/rpc/examples/js/pvm/Flipper.polkavm and b/substrate/frame/revive/rpc/examples/js/pvm/Flipper.polkavm differ diff --git a/substrate/frame/revive/rpc/examples/js/pvm/PiggyBank.polkavm b/substrate/frame/revive/rpc/examples/js/pvm/PiggyBank.polkavm index 3f96fdfc21d8..762b3008eee9 100644 Binary files a/substrate/frame/revive/rpc/examples/js/pvm/PiggyBank.polkavm and b/substrate/frame/revive/rpc/examples/js/pvm/PiggyBank.polkavm differ diff --git a/substrate/frame/revive/rpc/examples/js/src/geth-diff.test.ts b/substrate/frame/revive/rpc/examples/js/src/geth-diff.test.ts index 84d2b7e69359..2a7e4b1f3836 100644 --- a/substrate/frame/revive/rpc/examples/js/src/geth-diff.test.ts +++ b/substrate/frame/revive/rpc/examples/js/src/geth-diff.test.ts @@ -7,7 +7,7 @@ import { polkadotSdkPath, } from './util.ts' import { afterAll, afterEach, describe, expect, test } from 'bun:test' -import { encodeFunctionData, Hex, parseEther } from 'viem' +import { encodeAbiParameters, encodeFunctionData, Hex, parseEther } from 'viem' import { ErrorTesterAbi } from '../abi/ErrorTester' import { TracingCallerAbi } from '../abi/TracingCaller.ts' import { TracingCalleeAbi } from '../abi/TracingCallee.ts' @@ -371,23 +371,39 @@ for (const env of envs) { })() console.error('Caller address:', callerAddress) - const txHash = await (async () => { - const { request } = await env.serverWallet.simulateContract({ - address: callerAddress, - abi: TracingCallerAbi, - functionName: 'start', - args: [2n], - }) + //const txHash = await (async () => { + // const { request } = await env.serverWallet.simulateContract({ + // address: callerAddress, + // abi: TracingCallerAbi, + // functionName: 'start', + // args: [2n], + // }) + // + // const hash = await env.serverWallet.writeContract(request) + // await env.serverWallet.waitForTransactionReceipt({ hash }) + // + // + // return hash + //})() - const hash = await env.serverWallet.writeContract(request) - await env.serverWallet.waitForTransactionReceipt({ hash }) + let data = encodeFunctionData({ + abi: TracingCallerAbi, + functionName: 'start', + args: [2n], + }) - return hash - })() + const res = await env.debugClient.traceCall({ + account: env.accountWallet.account, + data, + to: callerAddress, + }) - console.error('Transaction hash:', txHash) - const res = await env.debugClient.traceTransaction(txHash) console.error(res) + Bun.write('/tmp/tracing.json', JSON.stringify(res, null, 2)) + //console.error('Transaction hash:', txHash) + //const res = await env.debugClient.traceTransaction(txHash) + //// serialize the result to + //console.error(res) }) }) } diff --git a/substrate/frame/revive/rpc/examples/js/src/lib.ts b/substrate/frame/revive/rpc/examples/js/src/lib.ts index e3c794e05121..e1f0e780d95b 100644 --- a/substrate/frame/revive/rpc/examples/js/src/lib.ts +++ b/substrate/frame/revive/rpc/examples/js/src/lib.ts @@ -126,17 +126,3 @@ export function assert(condition: any, message: string): asserts condition { throw new Error(message) } } - -const debugClient = createClient({ - chain: mainnet, - transport: http(), -}).extend((client) => ({ - // ... - async traceCall(args: CallParameters) { - return client.request({ - method: 'debug_traceCall', - params: [formatTransactionRequest(args), 'latest', {}], - }) - }, - // ... -})) diff --git a/substrate/frame/revive/rpc/examples/js/src/util.ts b/substrate/frame/revive/rpc/examples/js/src/util.ts index 11c618e754c6..ed48adfd1157 100644 --- a/substrate/frame/revive/rpc/examples/js/src/util.ts +++ b/substrate/frame/revive/rpc/examples/js/src/util.ts @@ -1,7 +1,16 @@ import { spawnSync } from 'bun' import { resolve } from 'path' import { readFileSync } from 'fs' -import { createClient, createWalletClient, defineChain, type Hex, http, publicActions } from 'viem' +import { + CallParameters, + createClient, + createWalletClient, + defineChain, + formatTransactionRequest, + type Hex, + http, + publicActions, +} from 'viem' import { privateKeyToAccount, nonceManager } from 'viem/accounts' export function getByteCode(name: string, evm: boolean = false): Hex { @@ -85,6 +94,7 @@ export async function createEnv(name: 'geth' | 'kitchensink') { chain, }).extend(publicActions) + const tracerConfig = { withLog: true } const debugClient = createClient({ chain, transport, @@ -92,7 +102,17 @@ export async function createEnv(name: 'geth' | 'kitchensink') { async traceTransaction(txHash: Hex) { return client.request({ method: 'debug_traceTransaction' as any, - params: [txHash, { tracer: 'callTracer' } as any], + params: [txHash, { tracer: 'callTracer', tracerConfig } as any], + }) + }, + async traceCall(args: CallParameters) { + return client.request({ + method: 'debug_traceCall' as any, + params: [ + formatTransactionRequest(args), + 'latest', + { tracer: 'callTracer', tracerConfig } as any, + ], }) }, // ... diff --git a/substrate/frame/revive/src/debug.rs b/substrate/frame/revive/src/debug.rs index 309c68921b71..496e3d80ac50 100644 --- a/substrate/frame/revive/src/debug.rs +++ b/substrate/frame/revive/src/debug.rs @@ -21,7 +21,7 @@ pub use crate::{ primitives::ExecReturnValue, BalanceOf, }; -use crate::{Config, GasMeter, LOG_TARGET}; +use crate::{Config, DispatchError, GasMeter, LOG_TARGET}; use alloc::vec::Vec; use sp_core::{H160, U256}; @@ -53,6 +53,9 @@ pub trait Tracing: Default { /// Called after a contract call is executed fn exit_child_span(&mut self, output: &ExecReturnValue, gas_meter: &GasMeter); + + /// Called when a contract call terminates with an error + fn exit_child_span_with_error(&mut self, error: DispatchError, gas_meter: &GasMeter); } impl Tracer { @@ -111,7 +114,6 @@ where } } - //fn after_call(&mut self, output: &ExecReturnValue); fn exit_child_span(&mut self, output: &ExecReturnValue, gas_meter: &GasMeter) { match self { Tracer::CallTracer(tracer) => { @@ -122,6 +124,17 @@ where }, } } + + fn exit_child_span_with_error(&mut self, error: DispatchError, gas_meter: &GasMeter) { + match self { + Tracer::CallTracer(tracer) => { + >::exit_child_span_with_error(tracer, error, gas_meter); + }, + Tracer::Disabled => { + log::trace!(target: LOG_TARGET, "call failed {error:?}") + }, + } + } } #[derive(Default, Debug, Clone, PartialEq, Eq)] @@ -171,8 +184,21 @@ where // Set the output of the current trace let current_index = self.current_stack.pop().unwrap(); let trace = &mut self.traces[current_index]; - trace.output = output.data.clone(); + trace.output = output.clone(); + trace.gas_used = gas_meter.gas_consumed(); + + // move the current trace into its parent + if let Some(parent_index) = self.current_stack.last() { + let child_trace = self.traces.remove(current_index); + self.traces[*parent_index].calls.push(child_trace); + } + } + fn exit_child_span_with_error(&mut self, error: DispatchError, gas_meter: &GasMeter) { + // Set the output of the current trace + let current_index = self.current_stack.pop().unwrap(); + let trace = &mut self.traces[current_index]; trace.gas_used = gas_meter.gas_consumed(); + trace.error = Some(format!("{error:?}")); // move the current trace into its parent if let Some(parent_index) = self.current_stack.last() { 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 e2cbe76d19fa..5c40dca6f21d 100644 --- a/substrate/frame/revive/src/evm/api/debug_rpc_types.rs +++ b/substrate/frame/revive/src/evm/api/debug_rpc_types.rs @@ -1,5 +1,5 @@ #![allow(missing_docs)] -use crate::Weight; +use crate::{ExecReturnValue, Weight}; use alloc::vec::Vec; use codec::{Decode, Encode}; use scale_info::TypeInfo; @@ -27,28 +27,43 @@ pub enum CallType { /// The traces of a transaction. #[derive(TypeInfo, Encode, Decode, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[serde(untagged)] -pub enum Traces { - CallTraces(Vec>), +pub enum Traces +where + Output: Default + PartialEq, +{ + CallTraces(Vec>), } -impl Traces { +impl Traces { /// Return mapped traces with the given gas mapper. - pub fn map(self, gas_mapper: F) -> Traces + pub fn map( + self, + gas_mapper: impl Fn(Gas) -> T + Copy, + output_mapper: impl Fn(Output) -> V + Copy, + ) -> Traces where - F: Fn(Gas) -> T + Copy, + V: Default + PartialEq, { match self { - Traces::CallTraces(traces) => - Traces::CallTraces(traces.into_iter().map(|trace| trace.map(gas_mapper)).collect()), + Traces::CallTraces(traces) => Traces::CallTraces( + traces.into_iter().map(|trace| trace.map(gas_mapper, output_mapper)).collect(), + ), } } } +pub fn is_default(value: &T) -> bool { + *value == T::default() +} + /// A smart contract execution call trace. #[derive( TypeInfo, Default, Encode, Decode, Serialize, Deserialize, Clone, Debug, Eq, PartialEq, )] -pub struct CallTrace { +pub struct CallTrace +where + Output: Default + PartialEq, +{ /// Address of the sender. pub from: H160, /// Address of the receiver. @@ -61,25 +76,34 @@ pub struct CallTrace { /// Type of call. #[serde(rename = "type")] pub call_type: CallType, - /// Gas limit. + /// Amount of gas provided for the call. pub gas: Gas, /// Amount of gas used. #[serde(rename = "gasUsed")] pub gas_used: Gas, /// Return data. - #[serde(skip_serializing_if = "Vec::is_empty")] - pub output: Vec, - /// Amount of gas provided for the call. + #[serde(flatten, skip_serializing_if = "is_default")] + pub output: Output, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + // TODO: add revertReason /// List of sub-calls. #[serde(skip_serializing_if = "Vec::is_empty")] - pub calls: Vec>, + pub calls: Vec>, } -impl CallTrace { +impl CallTrace +where + Output: Default + PartialEq, +{ /// Return a new call gas with a mapped gas value. - pub fn map(self, gas_mapper: F) -> CallTrace + pub fn map( + self, + gas_mapper: impl Fn(Gas) -> T + Copy, + output_mapper: impl Fn(Output) -> V + Copy, + ) -> CallTrace where - F: Fn(Gas) -> T + Copy, + V: Default + PartialEq, { CallTrace { from: self.from, @@ -87,10 +111,11 @@ impl CallTrace { input: self.input, value: self.value, call_type: self.call_type, + error: self.error, gas: gas_mapper(self.gas), gas_used: gas_mapper(self.gas_used), - output: self.output, - calls: self.calls.into_iter().map(|call| call.map(gas_mapper)).collect(), + output: output_mapper(self.output), + calls: self.calls.into_iter().map(|call| call.map(gas_mapper, output_mapper)).collect(), } } } diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index 675e973876cc..98c9d0b69d35 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -1105,7 +1105,14 @@ where let output = T::Debug::intercept_call(&contract_address, entry_point, &input_data) .unwrap_or_else(|| executable.execute(self, entry_point, input_data)) - .map_err(|e| ExecError { error: e.error, origin: ErrorOrigin::Callee })?; + .map_err(|e| { + >::exit_child_span_with_error( + self.tracer, + e.error, + &top_frame_mut!(self).nested_gas, + ); + ExecError { error: e.error, origin: ErrorOrigin::Callee } + })?; >::exit_child_span( self.tracer, diff --git a/substrate/frame/revive/src/primitives.rs b/substrate/frame/revive/src/primitives.rs index 6ed595785957..678ee5163302 100644 --- a/substrate/frame/revive/src/primitives.rs +++ b/substrate/frame/revive/src/primitives.rs @@ -126,7 +126,18 @@ pub enum ContractAccessError { } /// Output of a contract call or instantiation which ran to completion. -#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Default)] +#[derive( + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + Encode, + Decode, + RuntimeDebug, + TypeInfo, + Default, +)] pub struct ExecReturnValue { /// Flags passed along by `seal_return`. Empty when `seal_return` was never called. pub flags: ReturnFlags, diff --git a/substrate/frame/revive/src/tests.rs b/substrate/frame/revive/src/tests.rs index 47b583ef3258..a924c0dc2bc9 100644 --- a/substrate/frame/revive/src/tests.rs +++ b/substrate/frame/revive/src/tests.rs @@ -4845,7 +4845,7 @@ fn tracing_works() { let result = builder::bare_call(addr).data((3u32, addr_callee).encode()).build(); println!("{}", serde_json::to_string_pretty(&result.traces).unwrap()); - let traces = result.traces.map(|_| Weight::default()); + let traces = result.traces.map(|_| Weight::default(), |res| res); assert_eq!( traces, diff --git a/substrate/frame/revive/uapi/Cargo.toml b/substrate/frame/revive/uapi/Cargo.toml index 948c2c6e4f83..3217fe6c3311 100644 --- a/substrate/frame/revive/uapi/Cargo.toml +++ b/substrate/frame/revive/uapi/Cargo.toml @@ -20,6 +20,7 @@ codec = { features = [ "max-encoded-len", ], optional = true, workspace = true } pallet-revive-proc-macro = { workspace = true } +serde = { features = [ "alloc", "derive", ], optional = true, workspace = true, default-features = false } [target.'cfg(target_arch = "riscv64")'.dependencies] polkavm-derive = { version = "0.18.0" } @@ -29,6 +30,7 @@ features = ["unstable-hostfn"] targets = ["riscv64imac-unknown-none-elf"] [features] -default = ["scale"] +default = ["scale", "serde"] scale = ["dep:codec", "scale-info"] +serde = ["dep:serde"] unstable-hostfn = [] diff --git a/substrate/frame/revive/uapi/src/flags.rs b/substrate/frame/revive/uapi/src/flags.rs index 6a0f47c38c2c..d2cdd1d71253 100644 --- a/substrate/frame/revive/uapi/src/flags.rs +++ b/substrate/frame/revive/uapi/src/flags.rs @@ -20,6 +20,7 @@ use bitflags::bitflags; bitflags! { /// Flags used by a contract to customize exit behaviour. #[cfg_attr(feature = "scale", derive(codec::Encode, codec::Decode, scale_info::TypeInfo))] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Default)] pub struct ReturnFlags: u32 { /// If this bit is set all changes made by the contract execution are rolled back.