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