arb_evm/multi_gas/
classify.rs

1//! Per-opcode multi-gas dimension rules, mirroring Nitro's gas functions.
2//!
3//! Each opcode's total gas is split across resource kinds. The non-computation
4//! amounts are derived from the EIP-2929/2200 constants below; whatever the
5//! observed total leaves over is computation (base cost, warm access, memory
6//! expansion, value transfer, forwarded call gas).
7
8use alloy_primitives::U256;
9use arb_primitives::multigas::MultiGas;
10
11const WARM: u64 = 100; // WarmStorageReadCostEIP2929
12const COLD_SLOAD: u64 = 2100; // ColdSloadCostEIP2929
13const COLD_ACCOUNT: u64 = 2600; // ColdAccountAccessCostEIP2929
14const SSTORE_SET: u64 = 20_000; // SstoreSetGasEIP2200
15const SSTORE_RESET_WRITE: u64 = 2_900; // SstoreResetGasEIP2200 - ColdSloadCostEIP2929
16const NEW_ACCOUNT: u64 = 25_000; // CallNewAccountGas / CreateBySelfdestructGas
17const SELFDESTRUCT_BASE_WRITE: u64 = 4_900; // SelfdestructGasEIP150 - WarmStorageReadCostEIP2929
18const COPY_WORD: u64 = 3; // CopyGas
19const LOG_TOPIC_HISTORY: u64 = 256; // LogDataGas * LogTopicBytes (8 * 32)
20const LOG_DATA: u64 = 8; // LogDataGas
21
22/// Classification inputs for the gas-relevant opcodes; all other opcodes are
23/// pure computation.
24#[derive(Debug, Clone, Copy)]
25pub enum OpKind {
26    /// SLOAD.
27    StorageRead { cold: bool },
28    /// SSTORE.
29    StorageWrite {
30        cold: bool,
31        original: U256,
32        present: U256,
33        new: U256,
34    },
35    /// BALANCE, EXTCODESIZE, EXTCODEHASH, DELEGATECALL, STATICCALL — cold
36    /// surcharge only (their warm cost is the constant, i.e. computation).
37    AccountAccess { cold: bool },
38    /// EXTCODECOPY — cold surcharge plus per-word copy, both storage read.
39    ExtCodeCopy { cold: bool, words: u64 },
40    /// LOG0..LOG4.
41    Log { topics: u8, data_len: u64 },
42    /// CALL, CALLCODE — cold surcharge plus new-account growth.
43    Call { cold: bool, new_account: bool },
44    /// SELFDESTRUCT — base write split, cold beneficiary read, new-account growth.
45    SelfDestruct { cold: bool, new_account: bool },
46    /// Any opcode whose whole cost is computation.
47    Other,
48}
49
50/// Split `gas` (the opcode's observed total) across resource kinds. The
51/// remainder after the typed amounts falls into computation, so the result
52/// always sums to `gas`.
53pub fn classify(kind: OpKind, gas: u64) -> MultiGas {
54    let non_comp = non_computation(kind);
55    // An opcode that runs out of gas is charged only the gas that was left,
56    // which can be less than its typed cost. Attribute just the consumed gas so
57    // the split never exceeds what the opcode actually used.
58    if non_comp.single_gas() > gas {
59        return MultiGas::computation_gas(gas);
60    }
61    let computation = gas - non_comp.single_gas();
62    non_comp.saturating_add(MultiGas::computation_gas(computation))
63}
64
65fn non_computation(kind: OpKind) -> MultiGas {
66    use arb_primitives::multigas::ResourceKind::*;
67    match kind {
68        OpKind::StorageRead { cold } => {
69            MultiGas::storage_access_read_gas(if cold { COLD_SLOAD - WARM } else { 0 })
70        }
71        OpKind::StorageWrite {
72            cold,
73            original,
74            present,
75            new,
76        } => {
77            let mut pairs = Vec::with_capacity(2);
78            if cold {
79                pairs.push((StorageAccessRead, COLD_SLOAD));
80            }
81            if present != new && original == present {
82                if original.is_zero() {
83                    pairs.push((StorageGrowth, SSTORE_SET));
84                } else {
85                    pairs.push((StorageAccessWrite, SSTORE_RESET_WRITE));
86                }
87            }
88            MultiGas::from_pairs(&pairs)
89        }
90        OpKind::AccountAccess { cold } => {
91            MultiGas::storage_access_read_gas(if cold { COLD_ACCOUNT - WARM } else { 0 })
92        }
93        OpKind::ExtCodeCopy { cold, words } => {
94            let cold_part = if cold { COLD_ACCOUNT - WARM } else { 0 };
95            MultiGas::storage_access_read_gas(cold_part + words * COPY_WORD)
96        }
97        OpKind::Log { topics, data_len } => {
98            MultiGas::history_growth_gas(LOG_TOPIC_HISTORY * topics as u64 + LOG_DATA * data_len)
99        }
100        OpKind::Call { cold, new_account } => {
101            let mut pairs = Vec::with_capacity(2);
102            if cold {
103                pairs.push((StorageAccessRead, COLD_ACCOUNT - WARM));
104            }
105            if new_account {
106                pairs.push((StorageGrowth, NEW_ACCOUNT));
107            }
108            MultiGas::from_pairs(&pairs)
109        }
110        OpKind::SelfDestruct { cold, new_account } => {
111            let mut pairs = vec![(StorageAccessWrite, SELFDESTRUCT_BASE_WRITE)];
112            if cold {
113                pairs.push((StorageAccessRead, COLD_ACCOUNT));
114            }
115            if new_account {
116                pairs.push((StorageGrowth, NEW_ACCOUNT));
117            }
118            MultiGas::from_pairs(&pairs)
119        }
120        OpKind::Other => MultiGas::zero(),
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use arb_primitives::multigas::ResourceKind::*;
127
128    use super::*;
129
130    fn dims(mg: &MultiGas) -> (u64, u64, u64, u64, u64) {
131        (
132            mg.get(Computation),
133            mg.get(StorageAccessRead),
134            mg.get(StorageAccessWrite),
135            mg.get(StorageGrowth),
136            mg.get(HistoryGrowth),
137        )
138    }
139
140    #[test]
141    fn sload_warm() {
142        let mg = classify(OpKind::StorageRead { cold: false }, WARM);
143        assert_eq!(dims(&mg), (100, 0, 0, 0, 0));
144    }
145
146    #[test]
147    fn sload_cold() {
148        let mg = classify(OpKind::StorageRead { cold: true }, COLD_SLOAD);
149        assert_eq!(dims(&mg), (100, 2000, 0, 0, 0));
150        assert_eq!(mg.single_gas(), COLD_SLOAD);
151    }
152
153    #[test]
154    fn sstore_cold_create() {
155        let mg = classify(
156            OpKind::StorageWrite {
157                cold: true,
158                original: U256::ZERO,
159                present: U256::ZERO,
160                new: U256::from(1),
161            },
162            COLD_SLOAD + SSTORE_SET,
163        );
164        assert_eq!(dims(&mg), (0, 2100, 0, 20000, 0));
165    }
166
167    #[test]
168    fn sstore_warm_reset() {
169        let mg = classify(
170            OpKind::StorageWrite {
171                cold: false,
172                original: U256::from(7),
173                present: U256::from(7),
174                new: U256::from(9),
175            },
176            SSTORE_RESET_WRITE,
177        );
178        assert_eq!(dims(&mg), (0, 0, 2900, 0, 0));
179    }
180
181    #[test]
182    fn sstore_cold_reset() {
183        let mg = classify(
184            OpKind::StorageWrite {
185                cold: true,
186                original: U256::from(7),
187                present: U256::from(7),
188                new: U256::from(9),
189            },
190            COLD_SLOAD + SSTORE_RESET_WRITE,
191        );
192        assert_eq!(dims(&mg), (0, 2100, 2900, 0, 0));
193    }
194
195    #[test]
196    fn sstore_warm_dirty_and_noop_are_computation() {
197        let dirty = classify(
198            OpKind::StorageWrite {
199                cold: false,
200                original: U256::from(1),
201                present: U256::from(2),
202                new: U256::from(3),
203            },
204            WARM,
205        );
206        assert_eq!(dims(&dirty), (100, 0, 0, 0, 0));
207        let noop = classify(
208            OpKind::StorageWrite {
209                cold: false,
210                original: U256::from(5),
211                present: U256::from(5),
212                new: U256::from(5),
213            },
214            WARM,
215        );
216        assert_eq!(dims(&noop), (100, 0, 0, 0, 0));
217    }
218
219    #[test]
220    fn account_access_cold_warm() {
221        assert_eq!(
222            dims(&classify(
223                OpKind::AccountAccess { cold: true },
224                COLD_ACCOUNT
225            )),
226            (100, 2500, 0, 0, 0)
227        );
228        assert_eq!(
229            dims(&classify(OpKind::AccountAccess { cold: false }, WARM)),
230            (100, 0, 0, 0, 0)
231        );
232    }
233
234    #[test]
235    fn extcodecopy_cold_with_copy() {
236        // cold account (2500 read) + 3 words * 3 read + memory/base in computation
237        let total = WARM + (COLD_ACCOUNT - WARM) + 3 * COPY_WORD + 12;
238        let mg = classify(
239            OpKind::ExtCodeCopy {
240                cold: true,
241                words: 3,
242            },
243            total,
244        );
245        assert_eq!(dims(&mg), (112, 2509, 0, 0, 0));
246    }
247
248    #[test]
249    fn log_two_topics() {
250        // LOG2 base 375 + 2*119 computation; 2*256 + 8*data history
251        let data_len = 10u64;
252        let total = 375 + 2 * 119 + LOG_TOPIC_HISTORY * 2 + LOG_DATA * data_len;
253        let mg = classify(
254            OpKind::Log {
255                topics: 2,
256                data_len,
257            },
258            total,
259        );
260        assert_eq!(dims(&mg), (613, 0, 0, 0, 592));
261    }
262
263    #[test]
264    fn call_cold_new_account_value() {
265        // warm 100 + cold 2500 + value 9000 + new account 25000
266        let total = WARM + (COLD_ACCOUNT - WARM) + 9000 + NEW_ACCOUNT;
267        let mg = classify(
268            OpKind::Call {
269                cold: true,
270                new_account: true,
271            },
272            total,
273        );
274        assert_eq!(dims(&mg), (9100, 2500, 0, 25000, 0));
275    }
276
277    #[test]
278    fn selfdestruct_cold_new_account() {
279        // base 5000 (100 comp + 4900 write) + cold 2600 read + new 25000 growth
280        let total = 5000 + COLD_ACCOUNT + NEW_ACCOUNT;
281        let mg = classify(
282            OpKind::SelfDestruct {
283                cold: true,
284                new_account: true,
285            },
286            total,
287        );
288        assert_eq!(dims(&mg), (100, 2600, 4900, 25000, 0));
289    }
290
291    #[test]
292    fn other_is_all_computation() {
293        let mg = classify(OpKind::Other, 42);
294        assert_eq!(dims(&mg), (42, 0, 0, 0, 0));
295    }
296
297    #[test]
298    fn out_of_gas_opcode_attributes_only_consumed_gas() {
299        // A cold SLOAD that runs out of gas consumes less than the 2100 cold
300        // cost; the cold surcharge must not be attributed in full.
301        let sload = classify(OpKind::StorageRead { cold: true }, 1_696);
302        assert_eq!(sload.single_gas(), 1_696);
303        assert_eq!(dims(&sload), (1_696, 0, 0, 0, 0));
304        // A warm-reset SSTORE that runs out of gas (needs 2900, had 2707).
305        let sstore = classify(
306            OpKind::StorageWrite {
307                cold: false,
308                original: U256::from(7),
309                present: U256::from(7),
310                new: U256::from(9),
311            },
312            2_707,
313        );
314        assert_eq!(sstore.single_gas(), 2_707);
315        // A new-account CALL that runs out before the 25000 growth is charged.
316        let call = classify(
317            OpKind::Call {
318                cold: true,
319                new_account: true,
320            },
321            5_000,
322        );
323        assert_eq!(call.single_gas(), 5_000);
324    }
325
326    #[test]
327    fn single_gas_always_matches_total() {
328        for (kind, gas) in [
329            (OpKind::StorageRead { cold: true }, COLD_SLOAD),
330            (
331                OpKind::Call {
332                    cold: true,
333                    new_account: true,
334                },
335                36600,
336            ),
337            (
338                OpKind::Log {
339                    topics: 4,
340                    data_len: 100,
341                },
342                9999,
343            ),
344            (OpKind::Other, 21000),
345        ] {
346            assert_eq!(classify(kind, gas).single_gas(), gas);
347        }
348    }
349}