1use alloy_evm::Database;
11use alloy_primitives::{Address, B256, U256};
12use arb_primitives::multigas::MultiGas;
13use parking_lot::Mutex;
14use revm::{
15 Context, Inspector, Journal,
16 bytecode::opcode,
17 context::{JournalEntry, JournalInner},
18 interpreter::{
19 CallInputs, CallOutcome, CallValue, CreateInputs, CreateOutcome, Interpreter,
20 interpreter::EthInterpreter,
21 interpreter_types::{InputsTr, Jumps},
22 },
23};
24
25type Ctx<B, T, C, DB, Ch> = Context<B, T, C, DB, Journal<DB>, Ch>;
29use std::sync::Arc;
30
31use crate::multi_gas::classify::{OpKind, classify};
32
33pub type MultiGasSink = Arc<Mutex<Option<MultiGas>>>;
36
37const WARM: u64 = 100; const CREATE_DATA_GAS: u64 = 200; #[derive(Debug, Default)]
42pub struct MultiGasInspector {
43 prev_gas: u64,
44 pending: Pending,
45 accumulated: MultiGas,
46 sink: Option<MultiGasSink>,
47 open_frames: u32,
51}
52
53#[derive(Debug, Default)]
54enum Pending {
55 #[default]
56 None,
57 DeltaCold {
61 account: bool,
62 },
63 Log {
64 topics: u8,
65 data_len: u64,
66 },
67 ExtCodeCopy {
68 cold: bool,
69 words: u64,
70 },
71 SelfDestruct {
72 cold: bool,
73 new_account: bool,
74 },
75 SStore {
78 cold: bool,
79 contract: Address,
80 key: U256,
81 new: U256,
82 current: Option<U256>,
83 },
84 Frame {
87 cold: bool,
88 is_create: bool,
89 is_plain_call: bool,
90 delta: u64,
91 },
92 Other,
93}
94
95impl MultiGasInspector {
96 pub fn with_sink(sink: MultiGasSink) -> Self {
99 Self {
100 sink: Some(sink),
101 ..Default::default()
102 }
103 }
104
105 pub fn take_multi_gas(&mut self) -> MultiGas {
107 self.flush_dangling_frame();
108 self.pending = Pending::None;
109 self.prev_gas = 0;
110 self.open_frames = 0;
111 core::mem::replace(&mut self.accumulated, MultiGas::zero())
112 }
113
114 fn publish(&mut self) {
117 if self.sink.is_some() {
118 let gas = self.take_multi_gas();
119 if let Some(sink) = &self.sink {
120 *sink.lock() = Some(gas);
121 }
122 }
123 }
124
125 fn add(&mut self, gas: MultiGas) {
126 self.accumulated = self.accumulated.saturating_add(gas);
127 }
128
129 fn frame_closed(&mut self) {
132 self.open_frames = self.open_frames.saturating_sub(1);
133 if self.open_frames == 0 {
134 self.publish();
135 }
136 }
137
138 fn flush_dangling_frame(&mut self) {
141 if let Pending::Frame {
142 cold,
143 is_create,
144 delta,
145 ..
146 } = self.pending
147 {
148 let gas = if is_create {
149 MultiGas::computation_gas(delta)
150 } else {
151 classify(
152 OpKind::Call {
153 cold,
154 new_account: false,
155 },
156 delta,
157 )
158 };
159 self.add(gas);
160 self.pending = Pending::None;
161 }
162 }
163}
164
165impl<B, T, C, DB: Database, Ch> Inspector<Ctx<B, T, C, DB, Ch>, EthInterpreter>
166 for MultiGasInspector
167{
168 fn step(&mut self, interp: &mut Interpreter<EthInterpreter>, ctx: &mut Ctx<B, T, C, DB, Ch>) {
169 self.flush_dangling_frame();
170 self.prev_gas = interp.gas.remaining();
171 let op = interp.bytecode.opcode();
172 let journal = &ctx.journaled_state.inner;
173 self.pending = match op {
174 opcode::SLOAD => Pending::DeltaCold { account: false },
175 opcode::BALANCE | opcode::EXTCODESIZE | opcode::EXTCODEHASH => {
176 Pending::DeltaCold { account: true }
177 }
178 opcode::EXTCODECOPY => Pending::ExtCodeCopy {
179 cold: address_cold(journal, addr_arg(interp, 0)),
180 words: word_count(peek(interp, 3)),
181 },
182 opcode::LOG0..=opcode::LOG4 => Pending::Log {
183 topics: op - opcode::LOG0,
184 data_len: to_u64(peek(interp, 1)),
185 },
186 opcode::SSTORE => {
187 let contract = interp.input.target_address();
188 let key = peek(interp, 0);
189 Pending::SStore {
190 cold: slot_cold(journal, contract, key),
191 contract,
192 key,
193 new: peek(interp, 1),
194 current: slot_values(journal, contract, key).map(|(_, present)| present),
195 }
196 }
197 opcode::SELFDESTRUCT => {
198 let beneficiary = addr_arg(interp, 0);
199 let contract = interp.input.target_address();
200 Pending::SelfDestruct {
201 cold: address_cold(journal, beneficiary),
202 new_account: account_empty(journal, beneficiary)
203 && !account_balance_zero(journal, contract),
204 }
205 }
206 opcode::CALL | opcode::CALLCODE => Pending::Frame {
207 cold: address_cold(journal, addr_arg(interp, 1)),
208 is_create: false,
209 is_plain_call: op == opcode::CALL,
210 delta: 0,
211 },
212 opcode::DELEGATECALL | opcode::STATICCALL => Pending::Frame {
213 cold: address_cold(journal, addr_arg(interp, 1)),
214 is_create: false,
215 is_plain_call: false,
216 delta: 0,
217 },
218 opcode::CREATE | opcode::CREATE2 => Pending::Frame {
219 cold: false,
220 is_create: true,
221 is_plain_call: false,
222 delta: 0,
223 },
224 _ => Pending::Other,
225 };
226 }
227
228 fn step_end(
229 &mut self,
230 interp: &mut Interpreter<EthInterpreter>,
231 ctx: &mut Ctx<B, T, C, DB, Ch>,
232 ) {
233 let delta = self.prev_gas.saturating_sub(interp.gas.remaining());
234 let pending = core::mem::replace(&mut self.pending, Pending::None);
235 let gas = match pending {
236 Pending::Frame {
237 cold,
238 is_create,
239 is_plain_call,
240 ..
241 } => {
242 self.pending = Pending::Frame {
243 cold,
244 is_create,
245 is_plain_call,
246 delta,
247 };
248 return;
249 }
250 Pending::None => return,
251 Pending::DeltaCold { account } => {
252 let cold = delta > WARM;
253 let kind = if account {
254 OpKind::AccountAccess { cold }
255 } else {
256 OpKind::StorageRead { cold }
257 };
258 classify(kind, delta)
259 }
260 Pending::Log { topics, data_len } => classify(OpKind::Log { topics, data_len }, delta),
261 Pending::ExtCodeCopy { cold, words } => {
262 classify(OpKind::ExtCodeCopy { cold, words }, delta)
263 }
264 Pending::SelfDestruct { cold, new_account } => {
265 classify(OpKind::SelfDestruct { cold, new_account }, delta)
266 }
267 Pending::SStore {
268 cold,
269 contract,
270 key,
271 new,
272 current,
273 } => {
274 let original = slot_values(&ctx.journaled_state.inner, contract, key)
275 .map(|(original, _)| original)
276 .unwrap_or(U256::ZERO);
277 let present = current.unwrap_or(original);
278 classify(
279 OpKind::StorageWrite {
280 cold,
281 original,
282 present,
283 new,
284 },
285 delta,
286 )
287 }
288 Pending::Other => classify(OpKind::Other, delta),
289 };
290 self.add(gas);
291 }
292
293 fn call(
294 &mut self,
295 ctx: &mut Ctx<B, T, C, DB, Ch>,
296 inputs: &mut CallInputs,
297 ) -> Option<CallOutcome> {
298 self.open_frames += 1;
299 if let Pending::Frame {
300 cold,
301 is_create: false,
302 is_plain_call,
303 delta,
304 } = self.pending
305 {
306 self.pending = Pending::None;
307 let value_transfer = matches!(inputs.value, CallValue::Transfer(v) if !v.is_zero());
308 let own = call_own_cost(delta, inputs.gas_limit);
309 let new_account = is_plain_call
310 && value_transfer
311 && account_empty(&ctx.journaled_state.inner, inputs.target_address);
312 self.add(classify(OpKind::Call { cold, new_account }, own));
313 }
314 None
315 }
316
317 fn call_end(
318 &mut self,
319 _ctx: &mut Ctx<B, T, C, DB, Ch>,
320 _inputs: &CallInputs,
321 _outcome: &mut CallOutcome,
322 ) {
323 self.frame_closed();
324 }
325
326 fn create(
327 &mut self,
328 _ctx: &mut Ctx<B, T, C, DB, Ch>,
329 inputs: &mut CreateInputs,
330 ) -> Option<CreateOutcome> {
331 self.open_frames += 1;
332 if let Pending::Frame {
333 is_create: true,
334 delta,
335 ..
336 } = self.pending
337 {
338 self.pending = Pending::None;
339 let own = delta.saturating_sub(inputs.gas_limit());
340 self.add(MultiGas::computation_gas(own));
341 }
342 None
343 }
344
345 fn create_end(
346 &mut self,
347 _ctx: &mut Ctx<B, T, C, DB, Ch>,
348 _inputs: &CreateInputs,
349 outcome: &mut CreateOutcome,
350 ) {
351 if outcome.result.is_ok() {
352 let deposit = (outcome.result.output.len() as u64).saturating_mul(CREATE_DATA_GAS);
353 self.add(MultiGas::storage_growth_gas(deposit));
354 }
355 self.frame_closed();
356 }
357}
358
359fn call_own_cost(delta: u64, child_gas_limit: u64) -> u64 {
364 delta.saturating_sub(child_gas_limit)
365}
366
367fn peek(interp: &Interpreter<EthInterpreter>, from_top: usize) -> U256 {
368 let data = interp.stack.data();
369 data.len()
370 .checked_sub(from_top + 1)
371 .map(|i| data[i])
372 .unwrap_or(U256::ZERO)
373}
374
375fn addr_arg(interp: &Interpreter<EthInterpreter>, from_top: usize) -> Address {
376 Address::from_word(B256::from(peek(interp, from_top).to_be_bytes::<32>()))
377}
378
379fn word_count(len: U256) -> u64 {
380 to_u64(len).div_ceil(32)
381}
382
383fn to_u64(v: U256) -> u64 {
384 u64::try_from(v).unwrap_or(u64::MAX)
385}
386
387fn address_cold(journal: &JournalInner<JournalEntry>, addr: Address) -> bool {
388 if journal.warm_addresses.is_warm(&addr) {
389 return false;
390 }
391 match journal.state.get(&addr) {
392 Some(account) => account.is_cold_transaction_id(journal.transaction_id),
393 None => true,
394 }
395}
396
397fn slot_cold(journal: &JournalInner<JournalEntry>, addr: Address, key: U256) -> bool {
398 match journal.state.get(&addr).and_then(|a| a.storage.get(&key)) {
399 Some(slot) => slot.is_cold_transaction_id(journal.transaction_id),
400 None => true,
401 }
402}
403
404fn slot_values(
405 journal: &JournalInner<JournalEntry>,
406 addr: Address,
407 key: U256,
408) -> Option<(U256, U256)> {
409 let slot = journal.state.get(&addr)?.storage.get(&key)?;
410 Some((slot.original_value, slot.present_value))
411}
412
413fn account_empty(journal: &JournalInner<JournalEntry>, addr: Address) -> bool {
414 match journal.state.get(&addr) {
415 Some(account) => {
416 account.info.balance.is_zero()
417 && account.info.nonce == 0
418 && account.info.code_hash == revm::primitives::KECCAK_EMPTY
419 }
420 None => true,
421 }
422}
423
424fn account_balance_zero(journal: &JournalInner<JournalEntry>, addr: Address) -> bool {
425 match journal.state.get(&addr) {
426 Some(account) => account.info.balance.is_zero(),
427 None => true,
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn call_own_cost_strips_forwarded_gas() {
437 let delta = 2_600 + 9_000 + 100 + 50_000;
439 assert_eq!(call_own_cost(delta, 50_000), 2_600 + 9_000 + 100);
440 }
441
442 #[test]
443 fn call_own_cost_strips_value_transfer_stipend() {
444 const CALL_STIPEND: u64 = 2_300;
448 let call_cost = 2_600 + 9_000; let delta = call_cost; assert_eq!(call_own_cost(delta, CALL_STIPEND), call_cost - CALL_STIPEND);
451 }
452
453 #[test]
454 fn call_own_cost_saturates() {
455 assert_eq!(call_own_cost(100, 50_000), 0);
456 }
457
458 #[test]
459 fn word_count_rounds_up() {
460 assert_eq!(word_count(U256::ZERO), 0);
461 assert_eq!(word_count(U256::from(1)), 1);
462 assert_eq!(word_count(U256::from(32)), 1);
463 assert_eq!(word_count(U256::from(33)), 2);
464 }
465}