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		_code_address: Option<H160>,
293		_is_delegate_call: bool,
294		_is_read_only: bool,
295		_value: U256,
296		_input: &[u8],
297		_gas_limit: u64,
298	) {
299		// Costs will be calculated in exit_step by subtracting child consumption from total.
300		self.storages_per_call.push(Default::default());
301		self.depth += 1;
302	}
303
304	fn exit_child_span(
305		&mut self,
306		output: &ExecReturnValue,
307		gas_used: u64,
308		weight_consumed: Weight,
309	) {
310		// Accumulate child consumption to the parent step
311		if let Some(parent) = self.pending.last_mut() {
312			parent.child_gas = parent.child_gas.saturating_add(gas_used);
313			parent.child_weight = parent.child_weight.saturating_add(weight_consumed);
314		}
315
316		if output.did_revert() {
317			self.record_error("execution reverted".to_string());
318		}
319
320		self.finish_transaction(output.did_revert(), Bytes(output.data.to_vec()), gas_used);
321
322		self.storages_per_call.pop();
323
324		if self.depth > 0 {
325			self.depth -= 1;
326		}
327	}
328
329	fn exit_child_span_with_error(
330		&mut self,
331		error: DispatchError,
332		gas_used: u64,
333		weight_consumed: Weight,
334	) {
335		// Accumulate child consumption to the parent step
336		if let Some(parent) = self.pending.last_mut() {
337			parent.child_gas = parent.child_gas.saturating_add(gas_used);
338			parent.child_weight = parent.child_weight.saturating_add(weight_consumed);
339		}
340
341		self.record_error(format!("{:?}", error));
342
343		self.finish_transaction(true, Bytes::default(), gas_used);
344
345		if self.depth > 0 {
346			self.depth -= 1;
347		}
348
349		self.storages_per_call.pop();
350	}
351
352	fn storage_write(&mut self, key: &Key, _old_value: Option<Vec<u8>>, new_value: Option<&[u8]>) {
353		let Some(step_index) = self.storage_snapshot_target() else { return };
354
355		if let Some(storage) = self.storages_per_call.last_mut() {
356			let key_bytes = crate::evm::Bytes(key.unhashed().to_vec());
357			let value_bytes = crate::evm::Bytes(
358				new_value.map(|v| v.to_vec()).unwrap_or_else(|| alloc::vec![0u8; 32]),
359			);
360			storage.insert(key_bytes, value_bytes);
361		}
362
363		self.snapshot_storage_into(step_index);
364	}
365
366	fn storage_read(&mut self, key: &Key, value: Option<&[u8]>) {
367		let Some(step_index) = self.storage_snapshot_target() else { return };
368
369		if let Some(storage) = self.storages_per_call.last_mut() {
370			let key_bytes = crate::evm::Bytes(key.unhashed().to_vec());
371			storage.entry(key_bytes).or_insert_with(|| {
372				crate::evm::Bytes(value.map(|v| v.to_vec()).unwrap_or_else(|| alloc::vec![0u8; 32]))
373			});
374		}
375
376		self.snapshot_storage_into(step_index);
377	}
378}
379
380#[cfg(test)]
381mod tests {
382	use super::*;
383	use crate::tracing::{EVMFrameTraceInfo, FrameTraceInfo, PVM_FUEL_NAME};
384	use core::cell::Cell;
385	use pallet_revive_uapi::ReturnFlags;
386	use pretty_assertions::assert_eq;
387	use revm::bytecode::opcode::{CALL, PUSH1, SSTORE};
388
389	/// A stub execution frame. [`Frame::burn`] advances gas and weight in lockstep, so a step's
390	/// expected cost is the same number on both meters.
391	struct Frame {
392		gas_left: Cell<u64>,
393		consumed: Cell<u64>,
394	}
395
396	impl Frame {
397		fn new(gas: u64) -> Self {
398			Self { gas_left: Cell::new(gas), consumed: Cell::new(0) }
399		}
400
401		fn burn(&self, amount: u64) {
402			self.gas_left.set(self.gas_left.get() - amount);
403			self.consumed.set(self.consumed.get() + amount);
404		}
405	}
406
407	impl FrameTraceInfo for Frame {
408		fn gas_left(&self) -> u64 {
409			self.gas_left.get()
410		}
411
412		fn weight_consumed(&self) -> Weight {
413			Weight::from_parts(self.consumed.get(), self.consumed.get())
414		}
415
416		fn last_frame_output(&self) -> Bytes {
417			Bytes::default()
418		}
419	}
420
421	impl EVMFrameTraceInfo for Frame {
422		fn memory_snapshot(&self, _limit: usize) -> Vec<Bytes> {
423			Vec::new()
424		}
425
426		fn stack_snapshot(&self) -> Vec<Bytes> {
427			Vec::new()
428		}
429	}
430
431	fn enter_frame(tracer: &mut ExecutionTracer) {
432		tracer.enter_child_span(
433			H160::zero(),
434			H160::zero(),
435			None,
436			false,
437			false,
438			U256::zero(),
439			&[],
440			u64::MAX,
441		);
442	}
443
444	fn reverted() -> ExecReturnValue {
445		ExecReturnValue { flags: ReturnFlags::REVERT, data: Vec::new() }
446	}
447
448	/// The storage slots a captured step recorded, by their first key byte.
449	fn slots_of(step: &ExecutionStep) -> Vec<u8> {
450		match &step.kind {
451			ExecutionStepKind::EVMOpcode { storage: Some(storage), .. } => {
452				storage.keys().map(|key| key.0[0]).collect()
453			},
454			_ => Vec::new(),
455		}
456	}
457
458	fn exit_frame(tracer: &mut ExecutionTracer, consumed: u64) {
459		tracer.exit_child_span(
460			&ExecReturnValue::default(),
461			consumed,
462			Weight::from_parts(consumed, consumed),
463		);
464	}
465
466	/// Once truncation begins `steps` stops advancing, so hooks that fire while execution
467	/// continues must not pile onto the last captured step.
468	#[test]
469	fn truncated_steps_do_not_graft_onto_the_last_captured_step() {
470		let config = ExecutionTracerConfig { limit: Some(2), ..Default::default() };
471		let mut tracer = ExecutionTracer::new(config);
472		let frame = Frame::new(1_000);
473
474		enter_frame(&mut tracer);
475
476		for slot in 1u8..=4 {
477			tracer.enter_opcode(slot as u64, SSTORE, &frame);
478			tracer.storage_write(&Key::from_fixed([slot; 32]), None, Some(&[slot]));
479			frame.burn(10);
480			tracer.exit_step(&frame, None);
481		}
482
483		// A child call reverts, well past the limit.
484		enter_frame(&mut tracer);
485		tracer.exit_child_span(&reverted(), 5, Weight::from_parts(5, 5));
486
487		exit_frame(&mut tracer, 45);
488
489		let trace = tracer.collect_trace();
490		assert_eq!(trace.struct_logs.len(), 2);
491		assert_eq!(slots_of(&trace.struct_logs[0]), alloc::vec![1]);
492		assert_eq!(
493			slots_of(&trace.struct_logs[1]),
494			alloc::vec![1, 2],
495			"the boundary step keeps its own write and none of the dropped ones",
496		);
497		assert_eq!(trace.struct_logs[1].error, None, "it did not revert; a later frame did");
498	}
499
500	/// Reaching the limit is not the same as dropping a step: until something is actually
501	/// entered past it the trace is still complete, so its last step takes the annotation.
502	#[test]
503	fn a_complete_trace_that_reached_its_limit_still_annotates() {
504		let config = ExecutionTracerConfig { limit: Some(1), ..Default::default() };
505		let mut tracer = ExecutionTracer::new(config);
506		let frame = Frame::new(1_000);
507
508		enter_frame(&mut tracer);
509		tracer.enter_opcode(0, PUSH1, &frame);
510		frame.burn(10);
511		tracer.exit_step(&frame, None);
512		tracer.exit_child_span(&reverted(), 10, Weight::from_parts(10, 10));
513
514		let trace = tracer.collect_trace();
515		assert_eq!(
516			trace.struct_logs[0].error.as_deref(),
517			Some("execution reverted"),
518			"nothing was dropped, so the last captured step is still the last executed one",
519		);
520	}
521
522	/// A captured call keeps its error annotation whether or not the callee ran a step before
523	/// failing, which the caller cannot observe.
524	#[test]
525	fn a_captured_call_keeps_its_error_annotation() {
526		for callee_steps in 0..2u64 {
527			let config = ExecutionTracerConfig { limit: Some(1), ..Default::default() };
528			let mut tracer = ExecutionTracer::new(config);
529			let frame = Frame::new(1_000);
530
531			enter_frame(&mut tracer);
532
533			// The CALL is step 0, so it is captured and is what reaches the limit.
534			tracer.enter_opcode(0, CALL, &frame);
535			enter_frame(&mut tracer);
536
537			for pc in 0..callee_steps {
538				tracer.enter_opcode(pc, PUSH1, &frame);
539				frame.burn(5);
540				tracer.exit_step(&frame, None);
541			}
542
543			frame.burn(5);
544			tracer.exit_child_span(&reverted(), 5, Weight::from_parts(5, 5));
545			tracer.exit_step(&frame, None);
546			exit_frame(&mut tracer, 20);
547
548			let trace = tracer.collect_trace();
549			assert_eq!(trace.struct_logs.len(), 1);
550			assert_eq!(
551				trace.struct_logs[0].error.as_deref(),
552				Some("execution reverted"),
553				"{callee_steps} callee steps: the CALL is captured and is what failed",
554			);
555			assert!(
556				!trace.failed,
557				"{callee_steps} callee steps: the reverting frame is not the outermost one",
558			);
559		}
560	}
561
562	/// A step's cost is a window on the meters, exclusive of the child frames it contains, so
563	/// captured steps describe disjoint windows and can never sum to more than the transaction
564	/// consumed. Truncation may only omit cost, never invent it.
565	#[test]
566	fn truncated_steps_keep_captured_costs_disjoint() {
567		let config = ExecutionTracerConfig { limit: Some(3), ..Default::default() };
568		let mut tracer = ExecutionTracer::new(config);
569		let frame = Frame::new(1_000);
570
571		enter_frame(&mut tracer); // the transaction's own frame
572
573		// Step 0: a plain opcode costing 10.
574		tracer.enter_opcode(0, PUSH1, &frame);
575		frame.burn(10);
576		tracer.exit_step(&frame, None);
577
578		// Step 1: the outer CALL, entered with 990 gas left.
579		tracer.enter_opcode(1, CALL, &frame);
580		enter_frame(&mut tracer);
581		frame.burn(10);
582
583		// Step 2: the inner CALL, entered with 980 gas left. This one reaches the limit.
584		tracer.enter_opcode(2, CALL, &frame);
585		enter_frame(&mut tracer);
586
587		// Steps in the innermost frame are past the limit and dropped, but the interpreter
588		// still reports their exits. Both entry points have to keep the stack aligned, so
589		// exercise an EVM opcode and a PVM syscall.
590		tracer.enter_opcode(0, PUSH1, &frame);
591		frame.burn(10);
592		tracer.exit_step(&frame, None);
593
594		tracer.enter_ecall(PVM_FUEL_NAME, &[], &frame);
595		frame.burn(10);
596		tracer.exit_step(&frame, None);
597
598		// Unwind. Each CALL keeps its own overhead: its window minus what its frame consumed.
599		frame.burn(12);
600		exit_frame(&mut tracer, 25);
601		tracer.exit_step(&frame, None); // inner CALL: (980 - 948) - 25 = 7
602		frame.burn(3);
603		exit_frame(&mut tracer, 40);
604		tracer.exit_step(&frame, None); // outer CALL: (990 - 945) - 40 = 5
605		frame.burn(5);
606		exit_frame(&mut tracer, 60);
607
608		tracer.dispatch_result(Weight::zero(), Weight::from_parts(60, 60));
609
610		let trace = tracer.collect_trace();
611		let costs = trace
612			.struct_logs
613			.iter()
614			.map(|step| (step.gas_cost, step.weight_cost))
615			.collect::<Vec<_>>();
616
617		assert_eq!(
618			costs,
619			alloc::vec![
620				(10, Weight::from_parts(10, 10)), // the plain opcode
621				(5, Weight::from_parts(5, 5)),    // outer CALL, its frame excluded
622				(7, Weight::from_parts(7, 7)),    // inner CALL, its frame excluded
623			],
624			"the two steps past the limit are dropped and the CALLs keep only their own cost",
625		);
626
627		assert_eq!(trace.gas, 60);
628		assert_eq!(trace.weight_consumed, Weight::from_parts(60, 60));
629	}
630}