referrerpolicy=no-referrer-when-downgrade

pallet_revive/evm/tracing/
execution_tracing.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17use crate::{
18	DispatchError, ExecReturnValue, Key, Weight,
19	evm::{
20		Bytes, ExecutionStep, ExecutionStepKind, ExecutionTrace, ExecutionTracerConfig,
21		tracing::Tracing,
22	},
23	tracing::{EVMFrameTraceInfo, FrameTraceInfo},
24	vm::pvm::env::lookup_trace_op_index,
25};
26use alloc::{
27	collections::BTreeMap,
28	format,
29	string::{String, ToString},
30	vec::Vec,
31};
32use sp_core::{H160, U256};
33
34/// Tracks a pending step (opcode/syscall) that hasn't completed yet.
35/// Used to accumulate child call consumption for CALL-like opcodes.
36#[derive(Default, Debug, Clone, PartialEq)]
37struct PendingStep {
38	/// Index of this step in the `steps` vector.
39	step_index: usize,
40	/// Accumulated gas consumed by child calls.
41	child_gas: u64,
42	/// Accumulated weight consumed by child calls.
43	child_weight: Weight,
44}
45
46/// A tracer that traces opcode and syscall execution step-by-step.
47#[derive(Default, Debug, Clone, PartialEq)]
48pub struct ExecutionTracer {
49	/// The tracer configuration.
50	config: ExecutionTracerConfig,
51
52	/// The collected trace steps.
53	steps: Vec<ExecutionStep>,
54
55	/// Stack of pending steps awaiting their exit_step call.
56	/// When entering an opcode/syscall, we push here.
57	/// When exit_step is called, we pop and finalize the step's gas/weight costs.
58	pending: Vec<PendingStep>,
59
60	/// Current call depth.
61	depth: u16,
62
63	/// Number of steps captured (for limiting).
64	step_count: u64,
65
66	/// Total gas used by the transaction.
67	total_gas_used: u64,
68
69	/// The base call weight of the transaction.
70	base_call_weight: Weight,
71
72	/// The Weight consumed by the transaction meter.
73	weight_consumed: Weight,
74
75	/// Whether the transaction failed.
76	failed: bool,
77
78	/// The return value of the transaction.
79	return_value: Bytes,
80
81	/// List of storage per call depth.
82	storages_per_call: Vec<BTreeMap<Bytes, Bytes>>,
83}
84
85impl ExecutionTracer {
86	/// Create a new [`ExecutionTracer`] instance.
87	pub fn new(config: ExecutionTracerConfig) -> Self {
88		Self {
89			config,
90			steps: Vec::new(),
91			pending: Vec::new(),
92			depth: 0,
93			step_count: 0,
94			total_gas_used: 0,
95			base_call_weight: Default::default(),
96			weight_consumed: Default::default(),
97			failed: false,
98			return_value: Bytes::default(),
99			storages_per_call: alloc::vec![Default::default()],
100		}
101	}
102
103	/// Collect the traces and return them.
104	pub fn collect_trace(self) -> ExecutionTrace {
105		let Self {
106			steps: struct_logs,
107			weight_consumed,
108			base_call_weight,
109			return_value,
110			total_gas_used: gas,
111			failed,
112			..
113		} = self;
114		ExecutionTrace { gas, weight_consumed, base_call_weight, failed, return_value, struct_logs }
115	}
116
117	/// Record an error in the current step.
118	fn record_error(&mut self, error: String) {
119		if let Some(last_step) = self.steps.last_mut() {
120			last_step.error = Some(error);
121		}
122	}
123}
124
125impl Tracing for ExecutionTracer {
126	fn is_execution_tracer(&self) -> bool {
127		true
128	}
129
130	fn dispatch_result(&mut self, base_call_weight: Weight, weight_consumed: Weight) {
131		self.base_call_weight = base_call_weight;
132		self.weight_consumed = weight_consumed;
133	}
134
135	fn enter_opcode(&mut self, pc: u64, opcode: u8, trace_info: &dyn EVMFrameTraceInfo) {
136		if self.config.limit.map(|l| self.step_count >= l).unwrap_or(false) {
137			return;
138		}
139
140		// Extract stack data if enabled
141		let stack_data =
142			if !self.config.disable_stack { trace_info.stack_snapshot() } else { Vec::new() };
143
144		// Extract memory data if enabled
145		let memory_data = if self.config.enable_memory {
146			trace_info.memory_snapshot(self.config.memory_word_limit as usize)
147		} else {
148			Vec::new()
149		};
150
151		// Extract return data if enabled
152		let return_data = if self.config.enable_return_data {
153			trace_info.last_frame_output()
154		} else {
155			crate::evm::Bytes::default()
156		};
157
158		let step = ExecutionStep {
159			gas: trace_info.gas_left(),
160			gas_cost: Default::default(),
161			weight_cost: trace_info.weight_consumed(), /* Store initial weight, will be updated
162			                                            * later */
163			depth: self.depth,
164			return_data,
165			error: None,
166			kind: ExecutionStepKind::EVMOpcode {
167				pc: pc as u32,
168				op: opcode,
169				stack: stack_data,
170				memory: memory_data,
171				storage: None,
172			},
173		};
174
175		let step_index = self.steps.len();
176		self.steps.push(step);
177		self.pending
178			.push(PendingStep { step_index, child_gas: 0, child_weight: Weight::zero() });
179		self.step_count += 1;
180	}
181
182	fn enter_ecall(&mut self, ecall: &'static str, args: &[u64], trace_info: &dyn FrameTraceInfo) {
183		if self.config.limit.map(|l| self.step_count >= l).unwrap_or(false) {
184			return;
185		}
186
187		// Extract return data if enabled
188		let return_data = if self.config.enable_return_data {
189			trace_info.last_frame_output()
190		} else {
191			crate::evm::Bytes::default()
192		};
193
194		// Extract syscall args if enabled
195		let syscall_args =
196			if !self.config.disable_syscall_details { args.to_vec() } else { Vec::new() };
197
198		let step = ExecutionStep {
199			gas: trace_info.gas_left(),
200			gas_cost: Default::default(),
201			weight_cost: trace_info.weight_consumed(), /* Store initial weight, will be updated
202			                                            * later */
203			depth: self.depth,
204			return_data,
205			error: None,
206			kind: ExecutionStepKind::PVMSyscall {
207				op: lookup_trace_op_index(ecall).unwrap_or_default(),
208				args: syscall_args,
209				returned: None,
210			},
211		};
212
213		let step_index = self.steps.len();
214		self.steps.push(step);
215		self.pending
216			.push(PendingStep { step_index, child_gas: 0, child_weight: Weight::zero() });
217		self.step_count += 1;
218	}
219
220	fn exit_step(&mut self, trace_info: &dyn FrameTraceInfo, returned: Option<u64>) {
221		let Some(pending) = self.pending.pop() else { return };
222		let Some(step) = self.steps.get_mut(pending.step_index) else { return };
223
224		// Calculate opcode cost: total consumption minus child consumption
225		let total_gas = step.gas.saturating_sub(trace_info.gas_left());
226		step.gas_cost = total_gas.saturating_sub(pending.child_gas);
227
228		// weight_cost currently holds initial weight; calculate total then subtract child
229		let total_weight = trace_info.weight_consumed().saturating_sub(step.weight_cost);
230		step.weight_cost = total_weight.saturating_sub(pending.child_weight);
231
232		if !self.config.disable_syscall_details &&
233			let ExecutionStepKind::PVMSyscall { returned: ref mut ret, .. } = step.kind
234		{
235			*ret = returned;
236		}
237	}
238
239	fn enter_child_span(
240		&mut self,
241		_from: H160,
242		_to: H160,
243		_delegate_call: Option<H160>,
244		_is_read_only: bool,
245		_value: U256,
246		_input: &[u8],
247		_gas_limit: u64,
248	) {
249		// Costs will be calculated in exit_step by subtracting child consumption from total.
250		self.storages_per_call.push(Default::default());
251		self.depth += 1;
252	}
253
254	fn exit_child_span(
255		&mut self,
256		output: &ExecReturnValue,
257		gas_used: u64,
258		weight_consumed: Weight,
259	) {
260		// Accumulate child consumption to the parent step
261		if let Some(parent) = self.pending.last_mut() {
262			parent.child_gas = parent.child_gas.saturating_add(gas_used);
263			parent.child_weight = parent.child_weight.saturating_add(weight_consumed);
264		}
265
266		if output.did_revert() {
267			self.record_error("execution reverted".to_string());
268			if self.depth == 0 {
269				self.failed = true;
270			}
271		} else {
272			self.return_value = Bytes(output.data.to_vec());
273		}
274
275		if self.depth == 1 {
276			self.total_gas_used = gas_used;
277		}
278
279		self.storages_per_call.pop();
280
281		if self.depth > 0 {
282			self.depth -= 1;
283		}
284	}
285
286	fn exit_child_span_with_error(
287		&mut self,
288		error: DispatchError,
289		gas_used: u64,
290		weight_consumed: Weight,
291	) {
292		// Accumulate child consumption to the parent step
293		if let Some(parent) = self.pending.last_mut() {
294			parent.child_gas = parent.child_gas.saturating_add(gas_used);
295			parent.child_weight = parent.child_weight.saturating_add(weight_consumed);
296		}
297
298		self.record_error(format!("{:?}", error));
299
300		// Mark as failed if this is the top-level call
301		if self.depth == 1 {
302			self.failed = true;
303			self.total_gas_used = gas_used;
304		}
305
306		if self.depth > 0 {
307			self.depth -= 1;
308		}
309
310		self.storages_per_call.pop();
311	}
312
313	fn storage_write(&mut self, key: &Key, _old_value: Option<Vec<u8>>, new_value: Option<&[u8]>) {
314		// Only track storage if not disabled
315		if self.config.disable_storage {
316			return;
317		}
318
319		if let Some(storage) = self.storages_per_call.last_mut() {
320			let key_bytes = crate::evm::Bytes(key.unhashed().to_vec());
321			let value_bytes = crate::evm::Bytes(
322				new_value.map(|v| v.to_vec()).unwrap_or_else(|| alloc::vec![0u8; 32]),
323			);
324			storage.insert(key_bytes, value_bytes);
325
326			if let Some(step) = self.steps.last_mut() {
327				if let ExecutionStepKind::EVMOpcode { storage: ref mut step_storage, .. } =
328					step.kind
329				{
330					*step_storage = Some(storage.clone());
331				}
332			}
333		}
334	}
335
336	fn storage_read(&mut self, key: &Key, value: Option<&[u8]>) {
337		// Only track storage if not disabled
338		if self.config.disable_storage {
339			return;
340		}
341
342		if let Some(storage) = self.storages_per_call.last_mut() {
343			let key_bytes = crate::evm::Bytes(key.unhashed().to_vec());
344			storage.entry(key_bytes).or_insert_with(|| {
345				crate::evm::Bytes(value.map(|v| v.to_vec()).unwrap_or_else(|| alloc::vec![0u8; 32]))
346			});
347
348			if let Some(step) = self.steps.last_mut() {
349				if let ExecutionStepKind::EVMOpcode { storage: ref mut step_storage, .. } =
350					step.kind
351				{
352					*step_storage = Some(storage.clone());
353				}
354			}
355		}
356	}
357}