Skip to main content

revm_inspectors/
opcode.rs

1use alloc::string::ToString;
2use alloy_primitives::map::HashMap;
3use alloy_rpc_types_trace::opcode::OpcodeGas;
4use revm::{
5    bytecode::opcode::{self, OpCode},
6    context::{ContextTr, JournalTr},
7    interpreter::{
8        interpreter_types::{Immediates, Jumps},
9        CallInputs, CallOutcome, CallScheme, CreateInputs, CreateOutcome, CreateScheme,
10        Interpreter,
11    },
12    Inspector,
13};
14
15/// An Inspector that counts opcodes and measures gas usage per opcode.
16#[derive(Clone, Debug, Default)]
17pub struct OpcodeGasInspector {
18    /// Map of opcode counts per transaction.
19    opcode_counts: HashMap<OpCode, u64>,
20    /// Map of total gas used per opcode.
21    opcode_gas: HashMap<OpCode, u64>,
22    /// Keep track of the last opcode executed and the remaining gas
23    last_opcode_gas_remaining: Option<(OpCode, u64)>,
24}
25
26impl OpcodeGasInspector {
27    /// Creates a new instance of the inspector.
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Returns the opcode counts collected during transaction execution.
33    pub const fn opcode_counts(&self) -> &HashMap<OpCode, u64> {
34        &self.opcode_counts
35    }
36
37    /// Returns the opcode gas usage collected during transaction execution.
38    pub const fn opcode_gas(&self) -> &HashMap<OpCode, u64> {
39        &self.opcode_gas
40    }
41
42    /// Returns an iterator over all opcodes with their count and combined gas usage.
43    ///
44    /// Note: this returns in no particular order.
45    pub fn opcode_iter(&self) -> impl Iterator<Item = (OpCode, (u64, u64))> + '_ {
46        self.opcode_counts.iter().map(move |(&opcode, &count)| {
47            let gas = self.opcode_gas.get(&opcode).copied().unwrap_or_default();
48            (opcode, (count, gas))
49        })
50    }
51
52    /// Returns an iterator over all opcodes with their count and combined gas usage.
53    ///
54    /// Note: this returns in no particular order.
55    pub fn opcode_gas_iter(&self) -> impl Iterator<Item = OpcodeGas> + '_ {
56        self.opcode_iter().map(|(opcode, (count, gas_used))| OpcodeGas {
57            opcode: opcode.to_string(),
58            count,
59            gas_used,
60        })
61    }
62
63    /// Helper function to subtract gas limit from opcode gas tracking.
64    /// This prevents call/create opcodes from including the gas consumed within the call/create.
65    fn subtract_gas_limit(&mut self, opcode_value: u8, gas_limit: u64) {
66        if let Some(opcode) = OpCode::new(opcode_value) {
67            let opcode_gas = self.opcode_gas.entry(opcode).or_default();
68            *opcode_gas = opcode_gas.saturating_sub(gas_limit);
69        }
70    }
71}
72
73impl<CTX> Inspector<CTX> for OpcodeGasInspector
74where
75    CTX: ContextTr,
76{
77    fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) {
78        let opcode_value = interp.bytecode.opcode();
79        if let Some(opcode) = OpCode::new(opcode_value) {
80            // keep track of opcode counts
81            *self.opcode_counts.entry(opcode).or_default() += 1;
82
83            // keep track of the last opcode executed
84            self.last_opcode_gas_remaining = Some((opcode, interp.gas.remaining()));
85        }
86    }
87
88    fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) {
89        // update gas usage for the last opcode
90        if let Some((opcode, gas_remaining)) = self.last_opcode_gas_remaining.take() {
91            let gas_cost = gas_remaining.saturating_sub(interp.gas.remaining());
92            *self.opcode_gas.entry(opcode).or_default() += gas_cost;
93        }
94    }
95
96    fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
97        if context.journal_ref().depth() == 0 {
98            // skip the root call
99            return None;
100        }
101
102        // for accurate call opcode gas tracking, we need to deduct the gas limit from the opcode
103        // gas, because otherwise the call opcodes would include the total gas consumed within the
104        // call itself, but we want to track how much gas the call opcode itself consumes.
105        let opcode = match inputs.scheme {
106            CallScheme::Call => opcode::CALL,
107            CallScheme::CallCode => opcode::CALLCODE,
108            CallScheme::DelegateCall => opcode::DELEGATECALL,
109            CallScheme::StaticCall => opcode::STATICCALL,
110        };
111
112        self.subtract_gas_limit(opcode, inputs.gas_limit);
113
114        None
115    }
116
117    fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option<CreateOutcome> {
118        if context.journal_ref().depth() == 0 {
119            // skip the root create
120            return None;
121        }
122
123        // for accurate create opcode gas tracking, we need to deduct the gas limit from the opcode
124        // gas, because otherwise the create opcodes would include the total gas consumed within the
125        // create itself, but we want to track how much gas the create opcode itself consumes.
126        let opcode = match inputs.scheme() {
127            CreateScheme::Create => opcode::CREATE,
128            CreateScheme::Create2 { .. } => opcode::CREATE2,
129            CreateScheme::Custom { .. } => return None,
130        };
131
132        self.subtract_gas_limit(opcode, inputs.gas_limit());
133
134        None
135    }
136}
137
138/// Accepts Bytecode that implements [Immediates] and returns the size of immediate
139/// value.
140///
141/// Primarily needed to handle a special case of RJUMPV opcode.
142pub fn immediate_size(bytecode: &impl Immediates) -> u8 {
143    let opcode = bytecode.read_u8();
144    let Some(opcode) = OpCode::new(opcode) else { return 0 };
145    opcode.info().immediate_size()
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use revm::{
152        bytecode::Bytecode,
153        database::CacheDB,
154        database_interface::EmptyDB,
155        interpreter::{interpreter::ExtBytecode, InputsImpl, SharedMemory},
156        primitives::{hardfork::SpecId, Bytes},
157        Context, MainContext,
158    };
159
160    #[test]
161    fn test_opcode_counter_inspector() {
162        let mut opcode_counter = OpcodeGasInspector::new();
163
164        let opcodes = [opcode::ADD, opcode::ADD, opcode::ADD, opcode::BYTE];
165
166        let bytecode = Bytecode::new_raw(Bytes::from(opcodes));
167        let mut interpreter = Interpreter::new(
168            SharedMemory::new(),
169            ExtBytecode::new(bytecode),
170            InputsImpl::default(),
171            false,
172            SpecId::default(),
173            u64::MAX,
174        );
175        let db = CacheDB::new(EmptyDB::default());
176
177        let mut context = Context::mainnet().with_db(db);
178        for _ in &opcodes {
179            opcode_counter.step(&mut interpreter, &mut context);
180        }
181    }
182
183    #[test]
184    fn test_with_variety_of_opcodes() {
185        let mut opcode_counter = OpcodeGasInspector::new();
186
187        let opcodes = [
188            opcode::PUSH1,
189            opcode::PUSH1,
190            opcode::ADD,
191            opcode::PUSH1,
192            opcode::SSTORE,
193            opcode::STOP,
194        ];
195
196        let bytecode = Bytecode::new_raw(Bytes::from(opcodes));
197        let mut interpreter = Interpreter::new(
198            SharedMemory::new(),
199            ExtBytecode::new(bytecode),
200            InputsImpl::default(),
201            false,
202            SpecId::default(),
203            u64::MAX,
204        );
205        let db = CacheDB::new(EmptyDB::default());
206
207        let mut context = Context::mainnet().with_db(db);
208        for _ in opcodes.iter() {
209            opcode_counter.step(&mut interpreter, &mut context);
210        }
211    }
212}