1use 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#[derive(Default, Debug, Clone, PartialEq)]
37struct PendingStep {
38 step_index: Option<usize>,
40 child_gas: u64,
42 child_weight: Weight,
44}
45
46#[derive(Default, Debug, Clone, PartialEq)]
48pub struct ExecutionTracer {
49 config: ExecutionTracerConfig,
51
52 steps: Vec<ExecutionStep>,
54
55 pending: Vec<PendingStep>,
59
60 depth: u16,
62
63 steps_dropped: bool,
65
66 total_gas_used: u64,
68
69 base_call_weight: Weight,
71
72 weight_consumed: Weight,
74
75 failed: bool,
77
78 return_value: Bytes,
80
81 storages_per_call: Vec<BTreeMap<Bytes, Bytes>>,
83}
84
85impl ExecutionTracer {
86 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 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 fn is_truncated(&self) -> bool {
119 self.config.limit.is_some_and(|limit| self.steps.len() as u64 >= limit)
120 }
121
122 fn current_step_index(&self) -> Option<usize> {
124 self.pending.last()?.step_index
125 }
126
127 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 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 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 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(), 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(), 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 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 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 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 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 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 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 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 #[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 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 #[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 #[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 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 #[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); tracer.enter_opcode(0, PUSH1, &frame);
575 frame.burn(10);
576 tracer.exit_step(&frame, None);
577
578 tracer.enter_opcode(1, CALL, &frame);
580 enter_frame(&mut tracer);
581 frame.burn(10);
582
583 tracer.enter_opcode(2, CALL, &frame);
585 enter_frame(&mut tracer);
586
587 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 frame.burn(12);
600 exit_frame(&mut tracer, 25);
601 tracer.exit_step(&frame, None); frame.burn(3);
603 exit_frame(&mut tracer, 40);
604 tracer.exit_step(&frame, None); 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)), (5, Weight::from_parts(5, 5)), (7, Weight::from_parts(7, 7)), ],
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}