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 `steps`, or `None` when the step was dropped by the limit.
39	step_index: Option<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	/// Whether any step has been dropped, after which `steps` no longer tracks execution.
64	steps_dropped: bool,
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			steps_dropped: false,
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	/// Whether [`ExecutionTracerConfig::limit`] has been reached, so further steps are dropped.
118	fn is_truncated(&self) -> bool {
119		self.config.limit.is_some_and(|limit| self.steps.len() as u64 >= limit)
120	}
121
122	/// Index of the step currently executing, or `None` when it was dropped by the limit.
123	fn current_step_index(&self) -> Option<usize> {
124		self.pending.last()?.step_index
125	}
126
127	/// Open a pending entry for a starting step, capturing it unless the limit was reached.
128	///
129	/// A dropped step still occupies an entry: [`Tracing`] guarantees an `exit_step` either way,
130	/// and that exit pops one.
131	fn push_step(&mut self, build: impl FnOnce(&ExecutionTracerConfig, u16) -> ExecutionStep) {
132		let step_index = if self.is_truncated() {
133			None
134		} else {
135			let step = build(&self.config, self.depth);
136			self.steps.push(step);
137			Some(self.steps.len() - 1)
138		};
139		self.steps_dropped |= step_index.is_none();
140
141		self.pending
142			.push(PendingStep { step_index, child_gas: 0, child_weight: Weight::zero() });
143	}
144
145	/// Record the transaction-level result. The outermost frame exits at depth 1, since every
146	/// frame enters through `enter_child_span` and `depth` is decremented after this runs.
147	fn finish_transaction(&mut self, failed: bool, return_value: Bytes, gas_used: u64) {
148		if self.depth != 1 {
149			return;
150		}
151
152		self.failed |= failed;
153		self.return_value = return_value;
154		self.total_gas_used = gas_used;
155	}
156
157	/// The step a storage snapshot belongs to, or `None` when the access can be ignored.
158	fn storage_snapshot_target(&self) -> Option<usize> {
159		if self.config.disable_storage {
160			return None;
161		}
162
163		self.current_step_index()
164	}
165
166	fn snapshot_storage_into(&mut self, step_index: usize) {
167		let Some(storage) = self.storages_per_call.last() else { return };
168
169		if let Some(step) = self.steps.get_mut(step_index) {
170			if let ExecutionStepKind::EVMOpcode { storage: ref mut step_storage, .. } = step.kind {
171				*step_storage = Some(storage.clone());
172			}
173		}
174	}
175
176	/// Record an error against the step that failed.
177	fn record_error(&mut self, error: String) {
178		let target = if self.steps_dropped {
179			self.current_step_index()
180		} else {
181			self.steps.len().checked_sub(1)
182		};
183
184		if let Some(step) = target.and_then(|index| self.steps.get_mut(index)) {
185			step.error = Some(error);
186		}
187	}
188}
189
190impl Tracing for ExecutionTracer {
191	fn is_execution_tracer(&self) -> bool {
192		true
193	}
194
195	fn dispatch_result(&mut self, base_call_weight: Weight, weight_consumed: Weight) {
196		self.base_call_weight = base_call_weight;
197		self.weight_consumed = weight_consumed;
198	}
199
200	fn enter_opcode(&mut self, pc: u64, opcode: u8, trace_info: &dyn EVMFrameTraceInfo) {
201		self.push_step(|config, depth| {
202			let stack_data =
203				if !config.disable_stack { trace_info.stack_snapshot() } else { Vec::new() };
204
205			let memory_data = if config.enable_memory {
206				trace_info.memory_snapshot(config.memory_word_limit as usize)
207			} else {
208				Vec::new()
209			};
210
211			let return_data = if config.enable_return_data {
212				trace_info.last_frame_output()
213			} else {
214				Bytes::default()
215			};
216
217			ExecutionStep {
218				gas: trace_info.gas_left(),
219				gas_cost: Default::default(),
220				weight_cost: trace_info.weight_consumed(), /* Store initial weight, will be
221				                                            * updated later */
222				depth,
223				return_data,
224				error: None,
225				kind: ExecutionStepKind::EVMOpcode {
226					pc: pc as u32,
227					op: opcode,
228					stack: stack_data,
229					memory: memory_data,
230					storage: None,
231				},
232			}
233		});
234	}
235
236	fn enter_ecall(&mut self, ecall: &'static str, args: &[u64], trace_info: &dyn FrameTraceInfo) {
237		self.push_step(|config, depth| {
238			let return_data = if config.enable_return_data {
239				trace_info.last_frame_output()
240			} else {
241				Bytes::default()
242			};
243
244			let syscall_args =
245				if !config.disable_syscall_details { args.to_vec() } else { Vec::new() };
246
247			ExecutionStep {
248				gas: trace_info.gas_left(),
249				gas_cost: Default::default(),
250				weight_cost: trace_info.weight_consumed(), /* Store initial weight, will be
251				                                            * updated later */
252				depth,
253				return_data,
254				error: None,
255				kind: ExecutionStepKind::PVMSyscall {
256					op: lookup_trace_op_index(ecall).unwrap_or_default(),
257					args: syscall_args,
258					returned: None,
259				},
260			}
261		});
262	}
263
264	fn exit_step(&mut self, trace_info: &dyn FrameTraceInfo, returned: Option<u64>) {
265		let Some(pending) = self.pending.pop() else {
266			debug_assert!(false, "exit_step without a matching enter_opcode/enter_ecall");
267			return;
268		};
269
270		// A dropped step has no cost to attribute; its accumulated child credit goes with it.
271		let Some(step_index) = pending.step_index else { return };
272		let Some(step) = self.steps.get_mut(step_index) else { return };
273
274		let total_gas = step.gas.saturating_sub(trace_info.gas_left());
275		step.gas_cost = total_gas.saturating_sub(pending.child_gas);
276
277		// weight_cost currently holds initial weight; calculate total then subtract child
278		let total_weight = trace_info.weight_consumed().saturating_sub(step.weight_cost);
279		step.weight_cost = total_weight.saturating_sub(pending.child_weight);
280
281		if !self.config.disable_syscall_details &&
282			let ExecutionStepKind::PVMSyscall { returned: ref mut ret, .. } = step.kind
283		{
284			*ret = returned;
285		}
286	}
287
288	fn enter_child_span(
289		&mut self,
290		_from: H160,
291		_to: H160,
292		_delegate_call: Option<H160>,
293		_is_read_only: bool,
294		_value: U256,
295		_input: &[u8],
296		_gas_limit: u64,
297	) {
298		// Costs will be calculated in exit_step by subtracting child consumption from total.
299		self.storages_per_call.push(Default::default());
300		self.depth += 1;
301	}
302
303	fn exit_child_span(
304		&mut self,
305		output: &ExecReturnValue,
306		gas_used: u64,
307		weight_consumed: Weight,
308	) {
309		// Accumulate child consumption to the parent step
310		if let Some(parent) = self.pending.last_mut() {
311			parent.child_gas = parent.child_gas.saturating_add(gas_used);
312			parent.child_weight = parent.child_weight.saturating_add(weight_consumed);
313		}
314
315		if output.did_revert() {
316			self.record_error("execution reverted".to_string());
317		}
318
319		self.finish_transaction(output.did_revert(), Bytes(output.data.to_vec()), gas_used);
320
321		self.storages_per_call.pop();
322
323		if self.depth > 0 {
324			self.depth -= 1;
325		}
326	}
327
328	fn exit_child_span_with_error(
329		&mut self,
330		error: DispatchError,
331		gas_used: u64,
332		weight_consumed: Weight,
333	) {
334		// Accumulate child consumption to the parent step
335		if let Some(parent) = self.pending.last_mut() {
336			parent.child_gas = parent.child_gas.saturating_add(gas_used);
337			parent.child_weight = parent.child_weight.saturating_add(weight_consumed);
338		}
339
340		self.record_error(format!("{:?}", error));
341
342		self.finish_transaction(true, Bytes::default(), gas_used);
343
344		if self.depth > 0 {
345			self.depth -= 1;
346		}
347
348		self.storages_per_call.pop();
349	}
350
351	fn storage_write(&mut self, key: &Key, _old_value: Option<Vec<u8>>, new_value: Option<&[u8]>) {
352		let Some(step_index) = self.storage_snapshot_target() else { return };
353
354		if let Some(storage) = self.storages_per_call.last_mut() {
355			let key_bytes = crate::evm::Bytes(key.unhashed().to_vec());
356			let value_bytes = crate::evm::Bytes(
357				new_value.map(|v| v.to_vec()).unwrap_or_else(|| alloc::vec![0u8; 32]),
358			);
359			storage.insert(key_bytes, value_bytes);
360		}
361
362		self.snapshot_storage_into(step_index);
363	}
364
365	fn storage_read(&mut self, key: &Key, value: Option<&[u8]>) {
366		let Some(step_index) = self.storage_snapshot_target() else { return };
367
368		if let Some(storage) = self.storages_per_call.last_mut() {
369			let key_bytes = crate::evm::Bytes(key.unhashed().to_vec());
370			storage.entry(key_bytes).or_insert_with(|| {
371				crate::evm::Bytes(value.map(|v| v.to_vec()).unwrap_or_else(|| alloc::vec![0u8; 32]))
372			});
373		}
374
375		self.snapshot_storage_into(step_index);
376	}
377}
378
379#[cfg(test)]
380mod tests {
381	use super::*;
382	use crate::tracing::{EVMFrameTraceInfo, FrameTraceInfo, PVM_FUEL_NAME};
383	use core::cell::Cell;
384	use pallet_revive_uapi::ReturnFlags;
385	use pretty_assertions::assert_eq;
386	use revm::bytecode::opcode::{CALL, PUSH1, SSTORE};
387
388	/// A stub execution frame. [`Frame::burn`] advances gas and weight in lockstep, so a step's
389	/// expected cost is the same number on both meters.
390	struct Frame {
391		gas_left: Cell<u64>,
392		consumed: Cell<u64>,
393	}
394
395	impl Frame {
396		fn new(gas: u64) -> Self {
397			Self { gas_left: Cell::new(gas), consumed: Cell::new(0) }
398		}
399
400		fn burn(&self, amount: u64) {
401			self.gas_left.set(self.gas_left.get() - amount);
402			self.consumed.set(self.consumed.get() + amount);
403		}
404	}
405
406	impl FrameTraceInfo for Frame {
407		fn gas_left(&self) -> u64 {
408			self.gas_left.get()
409		}
410
411		fn weight_consumed(&self) -> Weight {
412			Weight::from_parts(self.consumed.get(), self.consumed.get())
413		}
414
415		fn last_frame_output(&self) -> Bytes {
416			Bytes::default()
417		}
418	}
419
420	impl EVMFrameTraceInfo for Frame {
421		fn memory_snapshot(&self, _limit: usize) -> Vec<Bytes> {
422			Vec::new()
423		}
424
425		fn stack_snapshot(&self) -> Vec<Bytes> {
426			Vec::new()
427		}
428	}
429
430	fn enter_frame(tracer: &mut ExecutionTracer) {
431		tracer.enter_child_span(
432			H160::zero(),
433			H160::zero(),
434			None,
435			false,
436			U256::zero(),
437			&[],
438			u64::MAX,
439		);
440	}
441
442	fn reverted() -> ExecReturnValue {
443		ExecReturnValue { flags: ReturnFlags::REVERT, data: Vec::new() }
444	}
445
446	/// The storage slots a captured step recorded, by their first key byte.
447	fn slots_of(step: &ExecutionStep) -> Vec<u8> {
448		match &step.kind {
449			ExecutionStepKind::EVMOpcode { storage: Some(storage), .. } => {
450				storage.keys().map(|key| key.0[0]).collect()
451			},
452			_ => Vec::new(),
453		}
454	}
455
456	fn exit_frame(tracer: &mut ExecutionTracer, consumed: u64) {
457		tracer.exit_child_span(
458			&ExecReturnValue::default(),
459			consumed,
460			Weight::from_parts(consumed, consumed),
461		);
462	}
463
464	/// Once truncation begins `steps` stops advancing, so hooks that fire while execution
465	/// continues must not pile onto the last captured step.
466	#[test]
467	fn truncated_steps_do_not_graft_onto_the_last_captured_step() {
468		let config = ExecutionTracerConfig { limit: Some(2), ..Default::default() };
469		let mut tracer = ExecutionTracer::new(config);
470		let frame = Frame::new(1_000);
471
472		enter_frame(&mut tracer);
473
474		for slot in 1u8..=4 {
475			tracer.enter_opcode(slot as u64, SSTORE, &frame);
476			tracer.storage_write(&Key::from_fixed([slot; 32]), None, Some(&[slot]));
477			frame.burn(10);
478			tracer.exit_step(&frame, None);
479		}
480
481		// A child call reverts, well past the limit.
482		enter_frame(&mut tracer);
483		tracer.exit_child_span(&reverted(), 5, Weight::from_parts(5, 5));
484
485		exit_frame(&mut tracer, 45);
486
487		let trace = tracer.collect_trace();
488		assert_eq!(trace.struct_logs.len(), 2);
489		assert_eq!(slots_of(&trace.struct_logs[0]), alloc::vec![1]);
490		assert_eq!(
491			slots_of(&trace.struct_logs[1]),
492			alloc::vec![1, 2],
493			"the boundary step keeps its own write and none of the dropped ones",
494		);
495		assert_eq!(trace.struct_logs[1].error, None, "it did not revert; a later frame did");
496	}
497
498	/// Reaching the limit is not the same as dropping a step: until something is actually
499	/// entered past it the trace is still complete, so its last step takes the annotation.
500	#[test]
501	fn a_complete_trace_that_reached_its_limit_still_annotates() {
502		let config = ExecutionTracerConfig { limit: Some(1), ..Default::default() };
503		let mut tracer = ExecutionTracer::new(config);
504		let frame = Frame::new(1_000);
505
506		enter_frame(&mut tracer);
507		tracer.enter_opcode(0, PUSH1, &frame);
508		frame.burn(10);
509		tracer.exit_step(&frame, None);
510		tracer.exit_child_span(&reverted(), 10, Weight::from_parts(10, 10));
511
512		let trace = tracer.collect_trace();
513		assert_eq!(
514			trace.struct_logs[0].error.as_deref(),
515			Some("execution reverted"),
516			"nothing was dropped, so the last captured step is still the last executed one",
517		);
518	}
519
520	/// A captured call keeps its error annotation whether or not the callee ran a step before
521	/// failing, which the caller cannot observe.
522	#[test]
523	fn a_captured_call_keeps_its_error_annotation() {
524		for callee_steps in 0..2u64 {
525			let config = ExecutionTracerConfig { limit: Some(1), ..Default::default() };
526			let mut tracer = ExecutionTracer::new(config);
527			let frame = Frame::new(1_000);
528
529			enter_frame(&mut tracer);
530
531			// The CALL is step 0, so it is captured and is what reaches the limit.
532			tracer.enter_opcode(0, CALL, &frame);
533			enter_frame(&mut tracer);
534
535			for pc in 0..callee_steps {
536				tracer.enter_opcode(pc, PUSH1, &frame);
537				frame.burn(5);
538				tracer.exit_step(&frame, None);
539			}
540
541			frame.burn(5);
542			tracer.exit_child_span(&reverted(), 5, Weight::from_parts(5, 5));
543			tracer.exit_step(&frame, None);
544			exit_frame(&mut tracer, 20);
545
546			let trace = tracer.collect_trace();
547			assert_eq!(trace.struct_logs.len(), 1);
548			assert_eq!(
549				trace.struct_logs[0].error.as_deref(),
550				Some("execution reverted"),
551				"{callee_steps} callee steps: the CALL is captured and is what failed",
552			);
553			assert!(
554				!trace.failed,
555				"{callee_steps} callee steps: the reverting frame is not the outermost one",
556			);
557		}
558	}
559
560	/// A step's cost is a window on the meters, exclusive of the child frames it contains, so
561	/// captured steps describe disjoint windows and can never sum to more than the transaction
562	/// consumed. Truncation may only omit cost, never invent it.
563	#[test]
564	fn truncated_steps_keep_captured_costs_disjoint() {
565		let config = ExecutionTracerConfig { limit: Some(3), ..Default::default() };
566		let mut tracer = ExecutionTracer::new(config);
567		let frame = Frame::new(1_000);
568
569		enter_frame(&mut tracer); // the transaction's own frame
570
571		// Step 0: a plain opcode costing 10.
572		tracer.enter_opcode(0, PUSH1, &frame);
573		frame.burn(10);
574		tracer.exit_step(&frame, None);
575
576		// Step 1: the outer CALL, entered with 990 gas left.
577		tracer.enter_opcode(1, CALL, &frame);
578		enter_frame(&mut tracer);
579		frame.burn(10);
580
581		// Step 2: the inner CALL, entered with 980 gas left. This one reaches the limit.
582		tracer.enter_opcode(2, CALL, &frame);
583		enter_frame(&mut tracer);
584
585		// Steps in the innermost frame are past the limit and dropped, but the interpreter
586		// still reports their exits. Both entry points have to keep the stack aligned, so
587		// exercise an EVM opcode and a PVM syscall.
588		tracer.enter_opcode(0, PUSH1, &frame);
589		frame.burn(10);
590		tracer.exit_step(&frame, None);
591
592		tracer.enter_ecall(PVM_FUEL_NAME, &[], &frame);
593		frame.burn(10);
594		tracer.exit_step(&frame, None);
595
596		// Unwind. Each CALL keeps its own overhead: its window minus what its frame consumed.
597		frame.burn(12);
598		exit_frame(&mut tracer, 25);
599		tracer.exit_step(&frame, None); // inner CALL: (980 - 948) - 25 = 7
600		frame.burn(3);
601		exit_frame(&mut tracer, 40);
602		tracer.exit_step(&frame, None); // outer CALL: (990 - 945) - 40 = 5
603		frame.burn(5);
604		exit_frame(&mut tracer, 60);
605
606		tracer.dispatch_result(Weight::zero(), Weight::from_parts(60, 60));
607
608		let trace = tracer.collect_trace();
609		let costs = trace
610			.struct_logs
611			.iter()
612			.map(|step| (step.gas_cost, step.weight_cost))
613			.collect::<Vec<_>>();
614
615		assert_eq!(
616			costs,
617			alloc::vec![
618				(10, Weight::from_parts(10, 10)), // the plain opcode
619				(5, Weight::from_parts(5, 5)),    // outer CALL, its frame excluded
620				(7, Weight::from_parts(7, 7)),    // inner CALL, its frame excluded
621			],
622			"the two steps past the limit are dropped and the CALLs keep only their own cost",
623		);
624
625		assert_eq!(trace.gas, 60);
626		assert_eq!(trace.weight_consumed, Weight::from_parts(60, 60));
627	}
628}