Skip to main content

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}