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 _delegate_call: Option<H160>,
293 _is_read_only: bool,
294 _value: U256,
295 _input: &[u8],
296 _gas_limit: u64,
297 ) {
298 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 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 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 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 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 #[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 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 #[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 #[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 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 #[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); tracer.enter_opcode(0, PUSH1, &frame);
573 frame.burn(10);
574 tracer.exit_step(&frame, None);
575
576 tracer.enter_opcode(1, CALL, &frame);
578 enter_frame(&mut tracer);
579 frame.burn(10);
580
581 tracer.enter_opcode(2, CALL, &frame);
583 enter_frame(&mut tracer);
584
585 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 frame.burn(12);
598 exit_frame(&mut tracer, 25);
599 tracer.exit_step(&frame, None); frame.burn(3);
601 exit_frame(&mut tracer, 40);
602 tracer.exit_step(&frame, None); 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)), (5, Weight::from_parts(5, 5)), (7, Weight::from_parts(7, 7)), ],
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}