revm_interpreter/gas.rs
1//! EVM gas calculation utilities.
2
3pub use context_interface::cfg::gas::*;
4
5/// Represents the state of gas during execution.
6///
7/// Implements the EIP-8037 reservoir model for dual-limit gas accounting:
8/// - `remaining`: regular gas left (`gas_left`). Does NOT include `reservoir`.
9/// - `reservoir`: state gas pool (separate from `remaining`). Starts as `execution_gas - gas_left`.
10/// - `state_gas_spent`: tracks total state gas spent
11///
12/// **Regular gas charges** (`record_cost`): deduct from `remaining`, checked against `remaining`.
13/// **State gas charges** (`record_state_cost`): deduct from `reservoir` first; when exhausted, spill into `remaining`.
14/// Total gas available = `remaining` + `reservoir`.
15///
16/// On mainnet (no state gas), `reservoir = 0` so all gas is regular gas and behavior is unchanged.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct Gas {
20 /// Tracker for gas during execution.
21 tracker: GasTracker,
22 /// Memoisation of values for memory expansion cost.
23 memory: MemoryGas,
24}
25
26impl Gas {
27 /// Creates a new `Gas` struct with the given gas limit.
28 ///
29 /// Sets `reservoir = 0` so all gas is regular gas (standard mainnet behavior).
30 #[inline]
31 pub const fn new(limit: u64) -> Self {
32 Self {
33 tracker: GasTracker::new(limit, limit, 0),
34 memory: MemoryGas::new(),
35 }
36 }
37
38 /// Returns the tracker for gas during execution.
39 #[inline]
40 pub const fn tracker(&self) -> &GasTracker {
41 &self.tracker
42 }
43
44 /// Returns the mutable tracker for gas during execution.
45 #[inline]
46 pub const fn tracker_mut(&mut self) -> &mut GasTracker {
47 &mut self.tracker
48 }
49
50 /// Creates a new `Gas` struct with a regular gas budget and reservoir (EIP-8037 reservoir model).
51 ///
52 /// Following the EIP-8037 spec:
53 /// - `remaining = limit` (regular gas available, i.e. `gas_left`)
54 /// - `reservoir` = state gas pool (separate from `remaining`)
55 /// - Total gas available = `remaining + reservoir = limit + reservoir`
56 ///
57 /// # Arguments
58 /// * `limit`: regular gas budget (capped execution gas, i.e. `gas_left`)
59 /// * `reservoir`: state gas pool (execution gas exceeding the regular gas cap)
60 #[inline]
61 pub const fn new_with_regular_gas_and_reservoir(limit: u64, reservoir: u64) -> Self {
62 Self {
63 tracker: GasTracker::new(limit, limit, reservoir),
64 memory: MemoryGas::new(),
65 }
66 }
67
68 /// Creates a new `Gas` struct with the given gas limit, but without any gas remaining.
69 #[inline]
70 pub const fn new_spent_with_reservoir(limit: u64, reservoir: u64) -> Self {
71 Self {
72 tracker: GasTracker::new(limit, 0, reservoir),
73 memory: MemoryGas::new(),
74 }
75 }
76
77 /// Returns the gas limit.
78 #[inline]
79 pub const fn limit(&self) -> u64 {
80 self.tracker.limit()
81 }
82
83 /// Returns the memory gas.
84 #[inline]
85 pub const fn memory(&self) -> &MemoryGas {
86 &self.memory
87 }
88
89 /// Returns the memory gas.
90 #[inline]
91 pub const fn memory_mut(&mut self) -> &mut MemoryGas {
92 &mut self.memory
93 }
94
95 /// Returns the total amount of gas that was refunded.
96 #[inline]
97 pub const fn refunded(&self) -> i64 {
98 self.tracker.refunded()
99 }
100
101 /// Returns the total amount of gas spent.
102 #[inline]
103 #[deprecated(
104 since = "32.0.0",
105 note = "After EIP-8037 gas is split on
106 regular and state gas, this method is no longer valid.
107 Use [`Gas::total_gas_spent`] instead"
108 )]
109 pub const fn spent(&self) -> u64 {
110 self.tracker
111 .limit()
112 .saturating_sub(self.tracker.remaining())
113 }
114
115 /// Returns the regular gas spent.
116 #[inline]
117 pub const fn total_gas_spent(&self) -> u64 {
118 self.tracker
119 .limit()
120 .saturating_sub(self.tracker.remaining())
121 }
122
123 /// Returns the final amount of gas used by subtracting the refund from spent gas.
124 #[inline]
125 pub const fn used(&self) -> u64 {
126 self.total_gas_spent()
127 .saturating_sub(self.refunded() as u64)
128 }
129
130 /// Returns the total amount of gas spent, minus the refunded gas.
131 #[inline]
132 pub const fn spent_sub_refunded(&self) -> u64 {
133 self.total_gas_spent()
134 .saturating_sub(self.tracker.refunded() as u64)
135 }
136
137 /// Returns the amount of gas remaining.
138 #[inline]
139 pub const fn remaining(&self) -> u64 {
140 self.tracker.remaining()
141 }
142
143 /// Returns the state gas reservoir.
144 #[inline]
145 pub const fn reservoir(&self) -> u64 {
146 self.tracker.reservoir()
147 }
148
149 /// Sets the state gas reservoir (used when propagating from child frame).
150 #[inline]
151 pub const fn set_reservoir(&mut self, val: u64) {
152 self.tracker.set_reservoir(val);
153 }
154
155 /// Returns total state gas spent so far.
156 ///
157 /// Can be negative within a call frame when 0→x→0 storage restoration
158 /// refills more state gas than this frame charged (see
159 /// [`GasTracker::refill_reservoir`]).
160 #[inline]
161 pub const fn state_gas_spent(&self) -> i64 {
162 self.tracker.state_gas_spent()
163 }
164
165 /// Sets the total state gas spent (used when propagating from child frame).
166 #[inline]
167 pub const fn set_state_gas_spent(&mut self, val: i64) {
168 self.tracker.set_state_gas_spent(val);
169 }
170
171 /// Refills the reservoir with state gas returned by 0→x→0 storage restoration.
172 ///
173 /// See [`GasTracker::refill_reservoir`].
174 #[inline]
175 pub const fn refill_reservoir(&mut self, amount: u64) {
176 self.tracker.refill_reservoir(amount);
177 }
178
179 /// Erases a gas cost from remaining (returns gas from child frame).
180 #[inline]
181 pub const fn erase_cost(&mut self, returned: u64) {
182 self.tracker.erase_cost(returned);
183 }
184
185 /// Spends all remaining gas excluding the reservoir.
186 ///
187 /// On exceptional halt, the remaining gas must be zeroed
188 /// to prevent state operations from succeeding via remaining gas.
189 ///
190 /// Note that this does not affect the reservoir.
191 #[inline]
192 pub const fn spend_all(&mut self) {
193 self.tracker.spend_all();
194 }
195
196 /// Records a refund value.
197 ///
198 /// `refund` can be negative but `self.refunded` should always be positive
199 /// at the end of transact.
200 #[inline]
201 pub const fn record_refund(&mut self, refund: i64) {
202 self.tracker.record_refund(refund);
203 }
204
205 /// Set a refund value for final refund.
206 ///
207 /// Max refund value is limited to Nth part of gas spend.
208 ///
209 /// Related to EIP-3529: Reduction in refunds
210 #[inline]
211 pub fn set_final_refund(&mut self, max_refund_quotient: u64) {
212 // EIP-8037: gas_used = total_gas_spent - reservoir (reservoir is unused state gas)
213 let gas_used = self.total_gas_spent().saturating_sub(self.reservoir());
214 self.tracker
215 .set_refunded((self.refunded() as u64).min(gas_used / max_refund_quotient) as i64);
216 }
217
218 /// Set a refund value. This overrides the current refund value.
219 #[inline]
220 pub const fn set_refund(&mut self, refund: i64) {
221 self.tracker.set_refunded(refund);
222 }
223
224 /// Set a remaining value. This overrides the current remaining value.
225 #[inline]
226 pub const fn set_remaining(&mut self, remaining: u64) {
227 self.tracker.set_remaining(remaining);
228 }
229
230 /// Set a spent value. This overrides the current spent value.
231 #[inline]
232 pub const fn set_spent(&mut self, spent: u64) {
233 self.tracker
234 .set_remaining(self.tracker.limit().saturating_sub(spent));
235 }
236
237 /// Records a regular gas cost (EIP-8037 reservoir model).
238 ///
239 /// Deducts from `remaining` and checks against implicit `gas_left` budget.
240 /// Regular gas charges cannot draw from the reservoir.
241 ///
242 /// Returns `false` if the regular gas limit is exceeded.
243 /// On failure, values contain wrapped (invalid) state — callers must not read after OOG.
244 #[inline]
245 #[must_use = "prefer using `gas!` instead to return an out-of-gas error on failure"]
246 #[deprecated(since = "32.0.0", note = "use record_regular_cost instead")]
247 pub const fn record_cost(&mut self, cost: u64) -> bool {
248 self.record_regular_cost(cost)
249 }
250
251 /// Records an explicit cost without bounds checking (unsafe path).
252 ///
253 /// Returns `true` if the gas limit is exceeded. Values wrap on underflow.
254 /// Only the regular gas check is meaningful here; total remaining can underflow
255 /// without consequence if the caller handles it.
256 #[inline(always)]
257 #[must_use = "In case of not enough gas, the interpreter should halt with an out-of-gas error"]
258 pub const fn record_cost_unsafe(&mut self, cost: u64) -> bool {
259 let remaining = self.tracker.remaining();
260 let oog = remaining < cost;
261 self.tracker.set_remaining(remaining.wrapping_sub(cost));
262 oog
263 }
264
265 /// Records a state gas cost (EIP-8037 reservoir model).
266 ///
267 /// State gas charges deduct from the reservoir first. If the reservoir is exhausted,
268 /// remaining charges spill into `gas_left` (requiring total `remaining >= cost`).
269 /// Tracks state gas spent.
270 ///
271 /// Returns `false` if total remaining gas is insufficient.
272 #[inline]
273 #[must_use = "In case of not enough gas, the interpreter should halt with an out-of-gas error"]
274 pub const fn record_state_cost(&mut self, cost: u64) -> bool {
275 self.tracker.record_state_cost(cost)
276 }
277
278 /// Deducts from `remaining` only (used for child frame gas forwarding).
279 /// Does not affect reservoir or regular gas budget.
280 /// Used for forwarding gas to child frames.
281 #[inline]
282 #[must_use = "In case of not enough gas, the interpreter should halt with an out-of-gas error"]
283 pub const fn record_regular_cost(&mut self, cost: u64) -> bool {
284 self.tracker.record_regular_cost(cost)
285 }
286}
287
288/// Utility struct that speeds up calculation of memory expansion
289/// It contains the current memory length and its memory expansion cost.
290///
291/// It allows us to split gas accounting from memory structure.
292#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
294pub struct MemoryGas {
295 /// Current memory length
296 pub words_num: usize,
297 /// Current memory expansion cost
298 pub expansion_cost: u64,
299}
300
301impl MemoryGas {
302 /// Creates a new `MemoryGas` instance with zero memory allocation.
303 #[inline]
304 pub const fn new() -> Self {
305 Self {
306 words_num: 0,
307 expansion_cost: 0,
308 }
309 }
310
311 /// Sets the number of words and the expansion cost.
312 ///
313 /// Returns the difference between the new and old expansion cost.
314 #[inline]
315 pub const fn set_words_num(
316 &mut self,
317 words_num: usize,
318 mut expansion_cost: u64,
319 ) -> Option<u64> {
320 self.words_num = words_num;
321 core::mem::swap(&mut self.expansion_cost, &mut expansion_cost);
322 self.expansion_cost.checked_sub(expansion_cost)
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn test_record_state_cost() {
332 // Test 1: Cost from reservoir only
333 let mut gas = Gas::new_with_regular_gas_and_reservoir(1000, 500);
334 assert!(gas.record_state_cost(200));
335 assert_eq!(
336 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
337 (300, 1000, 200)
338 );
339
340 // Test 2: Exhaust reservoir exactly
341 let mut gas = Gas::new_with_regular_gas_and_reservoir(1000, 500);
342 assert!(gas.record_state_cost(500));
343 assert_eq!(
344 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
345 (0, 1000, 500)
346 );
347
348 // Test 3: Spill to remaining (reservoir < cost)
349 let mut gas = Gas::new_with_regular_gas_and_reservoir(1000, 300);
350 assert!(gas.record_state_cost(500));
351 assert_eq!(
352 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
353 (0, 800, 500)
354 );
355
356 // Test 4: No reservoir (mainnet standard)
357 let mut gas = Gas::new(1000);
358 assert!(gas.record_state_cost(200));
359 assert_eq!(
360 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
361 (0, 800, 200)
362 );
363
364 // Test 5: Zero cost
365 let mut gas = Gas::new_with_regular_gas_and_reservoir(100, 50);
366 assert!(gas.record_state_cost(0));
367 assert_eq!(
368 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
369 (50, 100, 0)
370 );
371
372 // Test 6: Out of gas (cost > remaining + reservoir)
373 let mut gas = Gas::new_with_regular_gas_and_reservoir(100, 50);
374 assert!(!gas.record_state_cost(200));
375
376 // Test 7: Multiple operations accumulate state_gas_spent
377 let mut gas = Gas::new_with_regular_gas_and_reservoir(2000, 1000);
378 assert!(gas.record_state_cost(100));
379 assert!(gas.record_state_cost(200));
380 assert!(gas.record_state_cost(150));
381 assert_eq!(gas.state_gas_spent(), 450);
382
383 // Test 8: Complex scenario exhausting reservoir then remaining
384 let mut gas = Gas::new_with_regular_gas_and_reservoir(500, 300);
385 assert!(gas.record_state_cost(150)); // 150 from reservoir
386 assert_eq!((gas.reservoir(), gas.remaining()), (150, 500));
387 assert!(gas.record_state_cost(200)); // 150 from reservoir, 50 from remaining
388 assert_eq!((gas.reservoir(), gas.remaining()), (0, 450));
389 assert!(gas.record_state_cost(100)); // 100 from remaining
390 assert_eq!(
391 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
392 (0, 350, 450)
393 );
394 }
395
396 /// A.1: Verify state_gas_spent is incremented even after failed record_state_cost.
397 /// On OOG, state_gas_spent is NOT incremented and reservoir is unchanged.
398 #[test]
399 fn test_record_state_cost_oog_inflates_state_gas_spent() {
400 // remaining=30, reservoir=0, cost=100 → OOG
401 let mut gas = Gas::new(30);
402 assert!(!gas.record_state_cost(100));
403 // On OOG, state_gas_spent is NOT incremented (operation failed)
404 assert_eq!(gas.state_gas_spent(), 0);
405
406 // With reservoir partially covering: reservoir=20, remaining=30, cost=100
407 // spill = 100 - 20 = 80, remaining(30) < 80 → OOG
408 let mut gas = Gas::new_with_regular_gas_and_reservoir(30, 20);
409 assert!(!gas.record_state_cost(100));
410 // On OOG, state_gas_spent is NOT incremented and reservoir is unchanged
411 assert_eq!(gas.state_gas_spent(), 0);
412 assert_eq!(gas.reservoir(), 20);
413 }
414
415 /// Refill reservoir restores state gas in-place (EIP-8037 0→x→0 restoration).
416 ///
417 /// The refill decrements `state_gas_spent` and may drive it negative if the
418 /// matching charge was made by a parent frame.
419 #[test]
420 fn test_refill_reservoir() {
421 // Simple case: charge then refill within the same frame.
422 let mut gas = Gas::new_with_regular_gas_and_reservoir(1000, 500);
423 assert!(gas.record_state_cost(200));
424 assert_eq!(
425 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
426 (300, 1000, 200)
427 );
428 gas.refill_reservoir(200);
429 assert_eq!(
430 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
431 (500, 1000, 0)
432 );
433
434 // Child-frame case: refill without a prior charge makes state_gas_spent
435 // negative. The parent's matching +charge is reconciled on return.
436 let mut gas = Gas::new_with_regular_gas_and_reservoir(1000, 100);
437 gas.refill_reservoir(300);
438 assert_eq!(
439 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
440 (400, 1000, -300)
441 );
442 }
443
444 /// A.3: State gas with zero regular remaining but non-zero reservoir.
445 #[test]
446 fn test_record_state_cost_zero_remaining_with_reservoir() {
447 // remaining=0, reservoir=500: state gas draws entirely from reservoir
448 let mut gas = Gas::new_with_regular_gas_and_reservoir(0, 500);
449 assert!(gas.record_state_cost(200));
450 assert_eq!(
451 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
452 (300, 0, 200)
453 );
454
455 // Exhaust reservoir exactly
456 assert!(gas.record_state_cost(300));
457 assert_eq!(
458 (gas.reservoir(), gas.remaining(), gas.state_gas_spent()),
459 (0, 0, 500)
460 );
461
462 // Now any cost → OOG (both remaining and reservoir are 0)
463 assert!(!gas.record_state_cost(1));
464 }
465}