foundry_evm_traces/folded_stack_trace.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
use alloy_primitives::hex::ToHexExt;
use revm_inspectors::tracing::{
types::{CallTraceNode, CallTraceStep, DecodedTraceStep, TraceMemberOrder},
CallTraceArena,
};
/// Builds a folded stack trace from a call trace arena.
pub fn build(arena: &CallTraceArena) -> Vec<String> {
let mut fst = EvmFoldedStackTraceBuilder::default();
fst.process_call_node(arena.nodes(), 0);
fst.build()
}
/// Wrapper for building a folded stack trace using EVM call trace node.
#[derive(Default)]
pub struct EvmFoldedStackTraceBuilder {
/// Raw folded stack trace builder.
fst: FoldedStackTraceBuilder,
}
impl EvmFoldedStackTraceBuilder {
/// Returns the folded stack trace.
pub fn build(self) -> Vec<String> {
self.fst.build()
}
/// Creates an entry for a EVM CALL in the folded stack trace. This method recursively processes
/// all the children nodes of the call node and at the end it exits.
pub fn process_call_node(&mut self, nodes: &[CallTraceNode], idx: usize) {
let node = &nodes[idx];
let func_name = if node.trace.kind.is_any_create() {
let contract_name = node.trace.decoded.label.as_deref().unwrap_or("Contract");
format!("new {contract_name}")
} else {
let selector = node
.selector()
.map(|selector| selector.encode_hex_with_prefix())
.unwrap_or_else(|| "fallback".to_string());
let signature =
node.trace.decoded.call_data.as_ref().map(|dc| &dc.signature).unwrap_or(&selector);
if let Some(label) = &node.trace.decoded.label {
format!("{label}.{signature}")
} else {
signature.clone()
}
};
self.fst.enter(func_name, node.trace.gas_used as i64);
// Track internal function step exits to do in this call context.
let mut step_exits = vec![];
// Process children nodes.
for order in &node.ordering {
match order {
TraceMemberOrder::Call(child_idx) => {
let child_node_idx = node.children[*child_idx];
self.process_call_node(nodes, child_node_idx);
}
TraceMemberOrder::Step(step_idx) => {
self.exit_previous_steps(&mut step_exits, *step_idx);
self.process_step(&node.trace.steps, *step_idx, &mut step_exits)
}
TraceMemberOrder::Log(_) => {}
}
}
// Exit pending internal function calls if any.
for _ in 0..step_exits.len() {
self.fst.exit();
}
// Exit from this call context in the folded stack trace.
self.fst.exit();
}
/// Creates an entry for an internal function call in the folded stack trace. This method only
/// enters the function in the folded stack trace, we cannot exit since we need to exit at a
/// future step. Hence, we keep track of the step end index in the `step_exits`.
fn process_step(
&mut self,
steps: &[CallTraceStep],
step_idx: usize,
step_exits: &mut Vec<usize>,
) {
let step = &steps[step_idx];
if let Some(decoded_step) = &step.decoded {
match decoded_step {
DecodedTraceStep::InternalCall(decoded_internal_call, step_end_idx) => {
let gas_used = steps[*step_end_idx].gas_used.saturating_sub(step.gas_used);
self.fst.enter(decoded_internal_call.func_name.clone(), gas_used as i64);
step_exits.push(*step_end_idx);
}
DecodedTraceStep::Line(_) => {}
}
}
}
/// Exits all the previous internal calls that should end before starting step_idx.
fn exit_previous_steps(&mut self, step_exits: &mut Vec<usize>, step_idx: usize) {
let initial_length = step_exits.len();
step_exits.retain(|&number| number > step_idx);
let num_exits = initial_length - step_exits.len();
for _ in 0..num_exits {
self.fst.exit();
}
}
}
/// Helps to translate a function enter-exit flow into a folded stack trace.
///
/// Example:
/// ```solidity
/// function top() { child_a(); child_b() } // consumes 500 gas
/// function child_a() {} // consumes 100 gas
/// function child_b() {} // consumes 200 gas
/// ```
///
/// For execution of the `top` function looks like:
/// 1. enter `top`
/// 2. enter `child_a`
/// 3. exit `child_a`
/// 4. enter `child_b`
/// 5. exit `child_b`
/// 6. exit `top`
///
/// The translated folded stack trace lines look like:
/// 1. top
/// 2. top;child_a
/// 3. top;child_b
///
/// Including the gas consumed by the function by itself.
/// 1. top 200 // 500 - 100 - 200
/// 2. top;child_a 100
/// 3. top;child_b 200
#[derive(Debug, Default)]
pub struct FoldedStackTraceBuilder {
/// Trace entries.
traces: Vec<TraceEntry>,
/// Number of exits to be done before entering a new function.
exits: usize,
}
#[derive(Debug, Default)]
struct TraceEntry {
/// Names of all functions in the call stack of this trace.
names: Vec<String>,
/// Gas consumed by this function, allowed to be negative due to refunds.
gas: i64,
}
impl FoldedStackTraceBuilder {
/// Enter execution of a function call that consumes `gas`.
pub fn enter(&mut self, label: String, gas: i64) {
let mut names = self.traces.last().map(|entry| entry.names.clone()).unwrap_or_default();
while self.exits > 0 {
names.pop();
self.exits -= 1;
}
names.push(label);
self.traces.push(TraceEntry { names, gas });
}
/// Exit execution of a function call.
pub fn exit(&mut self) {
self.exits += 1;
}
/// Returns folded stack trace.
pub fn build(mut self) -> Vec<String> {
self.subtract_children();
self.build_without_subtraction()
}
/// Internal method to build the folded stack trace without subtracting gas consumed by
/// the children function calls.
fn build_without_subtraction(&mut self) -> Vec<String> {
let mut lines = Vec::new();
for TraceEntry { names, gas } in self.traces.iter() {
lines.push(format!("{} {}", names.join(";"), gas));
}
lines
}
/// Subtracts gas consumed by the children function calls from the parent function calls.
fn subtract_children(&mut self) {
// Iterate over each trace to find the children and subtract their values from the parents.
for i in 0..self.traces.len() {
let (left, right) = self.traces.split_at_mut(i);
let TraceEntry { names, gas } = &right[0];
if names.len() > 1 {
let parent_trace_to_match = &names[..names.len() - 1];
for parent in left.iter_mut().rev() {
if parent.names == parent_trace_to_match {
parent.gas -= gas;
break;
}
}
}
}
}
}
mod tests {
#[test]
fn test_fst_1() {
let mut trace = super::FoldedStackTraceBuilder::default();
trace.enter("top".to_string(), 500);
trace.enter("child_a".to_string(), 100);
trace.exit();
trace.enter("child_b".to_string(), 200);
assert_eq!(
trace.build_without_subtraction(),
vec![
"top 500", //
"top;child_a 100",
"top;child_b 200",
]
);
assert_eq!(
trace.build(),
vec![
"top 200", // 500 - 100 - 200
"top;child_a 100",
"top;child_b 200",
]
);
}
#[test]
fn test_fst_2() {
let mut trace = super::FoldedStackTraceBuilder::default();
trace.enter("top".to_string(), 500);
trace.enter("child_a".to_string(), 300);
trace.enter("child_b".to_string(), 100);
trace.exit();
trace.exit();
trace.enter("child_c".to_string(), 100);
assert_eq!(
trace.build_without_subtraction(),
vec![
"top 500", //
"top;child_a 300",
"top;child_a;child_b 100",
"top;child_c 100",
]
);
assert_eq!(
trace.build(),
vec![
"top 100", // 500 - 300 - 100
"top;child_a 200", // 300 - 100
"top;child_a;child_b 100",
"top;child_c 100",
]
);
}
#[test]
fn test_fst_3() {
let mut trace = super::FoldedStackTraceBuilder::default();
trace.enter("top".to_string(), 1700);
trace.enter("child_a".to_string(), 500);
trace.exit();
trace.enter("child_b".to_string(), 500);
trace.enter("child_c".to_string(), 500);
trace.exit();
trace.exit();
trace.exit();
trace.enter("top2".to_string(), 1700);
assert_eq!(
trace.build_without_subtraction(),
vec![
"top 1700", //
"top;child_a 500",
"top;child_b 500",
"top;child_b;child_c 500",
"top2 1700",
]
);
assert_eq!(
trace.build(),
vec![
"top 700", //
"top;child_a 500",
"top;child_b 0",
"top;child_b;child_c 500",
"top2 1700",
]
);
}
}