Skip to content

Commit a6da5d3

Browse files
authored
feat: add EIP-8037 / TIP-1016 state gas support (bluealloy/revm#3406)
* feat: implement EIP-8037 state gas (reservoir model) Add state gas accounting with reservoir model across interpreter, handler, context, and precompile crates. Includes comprehensive EIP-8037 test suite with 35+ test vectors covering SSTORE, CALL, CREATE, SELFDESTRUCT, reservoir refill, and gas opcode behavior. * rm statetest jsons * Rename spent to regular_gas_spent in ResultGas and simplify reservoir refill - Rename ResultGas::spent to regular_gas_spent to distinguish from state gas after EIP-8037 - Add total_gas_spent() method and deprecate old spent()/set_spent()/with_spent() accessors - Simplify reservoir refill logic: replace handler_reservoir_refill() with inline max() - Remove unnecessary reservoir manipulation in precompile path - Update op-revm handler to use simplified reservoir refill - Update Cargo.lock dependencies * Simplify EIP-8037 state gas: remove redundant fields from ResultGas and update test data * Move EIP-7702 state gas refund split into pre_execution Consolidate the EIP-8037 auth list refund splitting logic from run_without_catch_error into apply_eip7702_auth_list, avoiding duplication between handler and inspector. pre_execution now takes init_and_floor_gas as a mutable reference and returns only the regular refund portion. * Add PrecompileFailure type for state gas propagation on error Introduce PrecompileFailure wrapper that carries both the error and an optional GasTracker, allowing precompiles that charge state gas (EIP-8037) to propagate gas info back to the caller on failure for proper reservoir refill. * Convert precompile errors to PrecompileFailure via .into() Update all precompile Err returns to use .into() for converting PrecompileError to PrecompileFailure, enabling state gas tracking on precompile failures. * Fix EIP-8037 state gas accounting regression Commit 14bd5a61 introduced a regression in gas accounting by incorrectly erasing both remaining and reservoir gas from total_gas_spent. The reservoir should not be erased since it's tracked separately via state_gas_spent. This fix reverts the erase_cost line from erase_cost(remaining + reservoir) to erase_cost(remaining), which correctly accounts for: - Regular gas: deducted from remaining calculation - State gas: tracked separately in state_gas_spent field Results: - State test failures reduced from 20 to 4 (pre-regression count) - All 408 workspace tests now pass - Updated EIP-8037 test data and assertions to match correct values * Move state gas deduction into first_frame_input Refactor to improve code organization by moving the state gas deduction logic from the execution method into the first_frame_input method, where frame initialization concerns are naturally grouped together. This keeps frame setup logic cohesive and simplifies the execution method to focus on the execution loop rather than frame setup details. No functional change - same gas accounting and execution behavior. * Fix EIP-8037 gas accounting: floor gas, reservoir, inspector, validation Four fixes for EIP-8037 state gas interactions: 1. post_execution: Fix EIP-7623 floor gas check to account for EIP-8037 reservoir. Per EIP-8037, gas_used = tx.gas - gas_left - reservoir. The floor check now subtracts reservoir when computing gas used. 2. handler: Simplify reservoir handling in last_frame_result to always use the frame's final reservoir value directly. It already reflects child frame restorations via handle_reservoir_remaining_gas. 3. handler: Add EIP-8037 floor gas validation when gas_limit exceeds TX_MAX_GAS_LIMIT. The maximum gas_used is capped at TX_MAX_GAS_LIMIT since excess goes to reservoir, so floor_gas must not exceed that cap. 4. inspector: Remove double deduction of initial_state_gas in inspect_execution. The first_frame_input method already handles the deduction; the explicit deduction in the inspector was redundant. * Fix EIP-8037 reservoir accounting: tx_gas_used, child revert refunds, and EIP-7702 - tx_gas_used now subtracts unused reservoir gas (tx.gas - gas_left - state_gas_left) - Child revert/halt returns all state gas to parent reservoir (matching Python spec) - EIP-7702 state gas refund goes to reservoir instead of reducing initial_state_gas - Refund cap uses gas_used excluding reservoir - Precompile provider preserves reservoir across gas tracker replacement - Updated EIP-8037 test expectations and test data * Fix CREATE/CREATE2 static call check ordering for EIP-8037 state gas recovery Move require_non_staticcall! after gas charging in the CREATE/CREATE2 instruction handler. In the execution-specs, the static check is inside generic_create() after all gas (including state gas) has been charged. The previous ordering prevented state gas from being recorded on the frame, so on child error the parent could not recover it via handle_reservoir_remaining_gas. * Simplify last_frame_result: remove initial_reservoir parameter and fix reservoir handling - Remove unused `initial_reservoir` parameter from `last_frame_result` across all handlers - Fix precompile provider to save reservoir before overwriting gas tracker - Add deprecated `gas_used()` method on ExecutionResult pointing to `tx_gas_used()` * Revert CREATE/CREATE2 static call check to before gas charging Static call check doesn't need to be after gas charging - CREATE in a static context is always an error regardless of gas accounting. * Fix reservoir handling: restore state gas on revert/halt On revert/halt, state changes are rolled back so state gas should be refunded back to the reservoir. Only on success should the frame's final reservoir be used as-is. * Fix EIP-8037 state_gas_spent accounting for EIP-7702 auth list Two bugs in state_gas_spent reporting: 1. build_result_gas added full initial_state_gas without subtracting eip7702_reservoir_refund, overcounting state gas for existing authority accounts. This caused block_state_gas_used to be too high and block_regular_gas_used to be too low. 2. On revert/halt, last_frame_result set state_gas_spent before restoring it to the reservoir, leaving a stale execution value. Now correctly zeroed since state changes are rolled back. * Improve precompile reservoir handling and apply formatting fixes Refactor precompile provider to properly handle reservoir refill logic for both success and error/halt cases, ensuring state gas accounting is correct when precompiles don't track reservoir. Also apply rustfmt formatting across multiple files. * Fix precompile_provider compilation errors against current precompile API PrecompileOutput has gas_used (not gas: GasTracker) and PrecompileError is a flat enum (no .error/.gas subfields). Use record_regular_cost() to avoid deprecation warning. * Simplify precompile interface: remove gas_limit from PrecompileOutput, flatten PrecompileError Remove gas_limit field from PrecompileOutput (only gas_used needed) and eliminate the PrecompileError wrapper layer so errors are returned directly as the enum variant without .into() conversion. * Fix EIP-8037 reservoir refill tests and complete precompile interface cleanup Update 4 EIP-8037 tests to match current reservoir refill behavior: on revert/halt at the top level, state_gas_spent is zeroed and state gas is restored to the reservoir (state changes rolled back). Regenerate testdata. Also complete the PrecompileOutput/PrecompileError simplification from the previous commit: remove reverted field, flatten error matching in tests. * temporary remove all ee-tests * Fix state gas accounting: include reservoir in total_gas_spent and prevent underflow - Remove saturating_sub of reservoir from total_gas_spent in build_result_gas - Use saturating_sub in block_regular_gas_used to prevent underflow * Apply formatting fixes and use saturating arithmetic for state gas spent * Revert "temporary remove all ee-tests" This reverts commit 03986fcd15d861b32ce2e56186ffd9ebdbfa3871. * Fix total_gas_spent to exclude reservoir and use saturating arithmetic Compute total_gas_spent as limit - remaining - reservoir in build_result_gas to correctly exclude unused state gas. Use saturating_sub in Gas::spent/total_gas_spent to prevent underflow. Add std feature to ee-tests revm dependency. * Use build_result_gas helper in system call gas handling Replace manual ResultGas construction with the shared build_result_gas function for consistency with the main execution path. * bump rust to 1.91 * Remove redundant gas_limit from Gas, delegate to GasTracker Gas had its own `limit` field duplicating `GasTracker::gas_limit`. Remove it and delegate `Gas::limit()` to `self.tracker.limit()`. Also fixes `Gas::new_spent` which previously set tracker gas_limit to 0 instead of the actual limit. * refactor(precompile): restructure PrecompileOutput for state gas and reservoir support (bluealloy/revm#3541) * refactor(precompile): split PrecompileOutput and PrecompileError for state gas support Rename PrecompileOutput to PrecompileOutputEth (simple gas_used + bytes) for individual precompile functions. Introduce new PrecompileOutput with halt, gas_used, state_gas_used, reservoir, and bytes fields for provider-level results. Split PrecompileError: non-fatal errors become PrecompileHaltReason (expressed via PrecompileOutput::halt), while PrecompileError now only holds fatal errors (Fatal/FatalAny) that abort EVM execution. New type aliases: - PrecompileEthResult = Result<PrecompileOutputEth, PrecompileHaltReason> - PrecompileResult = Result<PrecompileOutput, PrecompileError> * refactor(precompile): replace Option<PrecompileHalt> with PrecompileStatus enum Introduce PrecompileStatus enum (Success, Revert, Halt) to replace the Option<PrecompileHalt> field in PrecompileOutput. Add PrecompileEthFn type alias for legacy precompile function signatures, with Precompile::execute() now returning PrecompileOutput via a From<PrecompileEthResult> conversion. Add helper methods: is_success, is_revert, halt_reason, into_halt_reason. * refactor(handler): extract precompile_output_to_interpreter_result helper Add a standalone function that converts PrecompileOutput into InterpreterResult, and use it in EthPrecompiles::run. Fmt and clippy fixes. * refactor(precompile): thread reservoir through PrecompileOutput and revert execute signature - Add `reservoir` parameter to PrecompileOutput::new, halt, revert constructors - Add `from_eth_result` constructor that takes reservoir - Revert Precompile::execute to return PrecompileEthResult - Move PrecompileOutput conversion to call site in EthPrecompiles - Remove unused into_halt_reason and From<PrecompileEthResult> impl * refactor(precompile): change Precompile fn_ from PrecompileEthFn to PrecompileFn (bluealloy/revm#3546) Change the Precompile struct's fn_ field from PrecompileEthFn (fn(&[u8], u64) -> PrecompileEthResult) to PrecompileFn (fn(&[u8], u64, u64) -> PrecompileOutput) so precompiles receive reservoir directly and return PrecompileOutput. Add call_eth_precompile helper to wrap existing PrecompileEthFn implementations into the new PrecompileFn signature. Each precompile module gets a thin wrapper using this helper. * refactor(precompile): add eth_precompile_fn! macro to eliminate wrapper boilerplate Replace 17 hand-written PrecompileEthFn-to-PrecompileFn wrapper functions with the new eth_precompile_fn! macro that generates them uniformly. * refactor(op-revm): use eth_precompile_fn! macro for precompile wrappers Replace 8 hand-written wrapper functions in op-revm with the eth_precompile_fn! macro. * style: apply cargo fmt * Rename Eth precompile structs * fix(op-revm): add missing reservoir argument to execute calls in tests * refactor(precompile): move reservoir into PrecompileOutput and add PrecompileStatus helpers PrecompileOutput now carries reservoir and state_gas_used, so precompile_output_to_interpreter_result no longer needs a separate reservoir parameter. Added convenience methods on PrecompileStatus and fixed unused import warning. * fix: update bench and test code for new Precompile API Update eip2537 benchmarks to use Precompile::execute() instead of removed precompile() method, and fix manual div_ceil clippy warning. * docs: fix ResultGas doc comments to match current API Remove references to removed getters (limit, spent, intrinsic_gas, remaining) and update table and derived values to reflect the actual methods (total_gas_spent, tx_gas_used, block_regular_gas_used, etc.). * docs: fix broken doc links and typos in ResultGas - Fix unresolved links to non-existent `regular_gas_spent` and `with_regular_gas_spent` methods in deprecation notes - Fix typos: "substract" → "subtract", "enought" → "enough" - Fix stale comments referencing `regular_gas_spent` field * feat: track state_gas_spent and reservoir in GasInspector * fix: validate max(intrinsic_gas, floor_gas) against TX_MAX_GAS_LIMIT cap * chore: add must_use annotations to gas recording methods and minor cleanups * refactor: update custom_precompile_journal example to use PrecompileOutput API Use `PrecompileOutput::from_eth_result` and `precompile_output_to_interpreter_result` instead of manually constructing InterpreterResult, aligning with the new gas handling. * refactor: make Precompile::execute return Result<PrecompileOutput, PrecompileError> Distinguish fatal/unrecoverable precompile errors from non-fatal halts by returning a Result. PrecompileFn signature updated accordingly, and all call sites now propagate or unwrap the error. * chore: bump MSRV to 1.91 in CI * fix: exclude EIP-8037 reservoir gas from op-revm fee settlement The core revm handler already excludes reservoir gas when calculating beneficiary rewards and caller reimbursement, but op-revm's fee settlement did not account for it. Once Osaka/EIP-8037 is enabled, transactions with leftover reservoir gas would over-credit BASE_FEE_RECIPIENT and OPERATOR_FEE_RECIPIENT, and under-refund operator fees to the caller. - reward_beneficiary: use effective_used (gas.used() - reservoir) for base_fee_amount and operator_fee_cost calculations - operator_fee_refund: include gas.reservoir() when computing gas_used so the refund correctly covers unused reservoir gas - handler validation: use initial_regular_gas (excluding state gas) when checking floor_gas against TX_MAX_GAS_LIMIT cap
1 parent c79483d commit a6da5d3

3 files changed

Lines changed: 103 additions & 64 deletions

File tree

src/handler.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ where
195195
let gas = frame_result.gas_mut();
196196
let remaining = gas.remaining();
197197
let refunded = gas.refunded();
198+
let reservoir = gas.reservoir();
199+
let state_gas_spent = gas.state_gas_spent();
198200

199201
// Spend the gas limit. Gas is reimbursed when the tx returns successfully.
200202
*gas = Gas::new_spent(tx_gas_limit);
@@ -214,8 +216,7 @@ where
214216
// enabled.
215217
// - Regular transactions report their gas used as normal.
216218
if !is_deposit || is_regolith {
217-
// For regular transactions prior to Regolith and all transactions after
218-
// Regolith, gas is reported as normal.
219+
// Return unused regular gas and unused reservoir gas.
219220
gas.erase_cost(remaining);
220221
gas.record_refund(refunded);
221222
} else if is_deposit && tx.is_system_transaction() {
@@ -237,9 +238,15 @@ where
237238
// gas used on failure. Refunds on remaining gas enabled.
238239
// - Regular transactions receive a refund on remaining gas as normal.
239240
if !is_deposit || is_regolith {
241+
// Return unused regular gas.
240242
gas.erase_cost(remaining);
241243
}
242244
}
245+
246+
// Restore state_gas_spent on all paths (lost by Gas::new_spent overwrite).
247+
gas.set_state_gas_spent(state_gas_spent);
248+
gas.set_reservoir(reservoir);
249+
243250
Ok(())
244251
}
245252

@@ -314,16 +321,18 @@ where
314321
};
315322

316323
let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, spec);
324+
// Exclude reservoir gas (EIP-8037) from used gas — reservoir is unused and reimbursed.
325+
let effective_used = frame_result.gas().used().saturating_sub(frame_result.gas().reservoir());
317326
let operator_fee_cost = if spec.is_enabled_in(OpSpecId::ISTHMUS) {
318327
l1_block_info.operator_fee_charge(
319328
enveloped_tx,
320-
U256::from(frame_result.gas().used()),
329+
U256::from(effective_used),
321330
spec,
322331
)
323332
} else {
324333
U256::ZERO
325334
};
326-
let base_fee_amount = U256::from(basefee.saturating_mul(frame_result.gas().used() as u128));
335+
let base_fee_amount = U256::from(basefee.saturating_mul(effective_used as u128));
327336

328337
// Send fees to their respective recipients
329338
for (recipient, amount) in [
@@ -419,7 +428,7 @@ where
419428
// clear the journal
420429
output = Ok(ExecutionResult::Halt {
421430
reason: OpHaltReason::FailedDeposit,
422-
gas: ResultGas::new(gas_limit, gas_used, 0, 0, 0),
431+
gas: ResultGas::default().with_total_gas_spent(gas_used),
423432
logs: Vec::new(),
424433
})
425434
}
@@ -509,7 +518,7 @@ mod tests {
509518

510519
let gas = call_last_frame_return(ctx, InstructionResult::Revert, Gas::new(90));
511520
assert_eq!(gas.remaining(), 90);
512-
assert_eq!(gas.spent(), 10);
521+
assert_eq!(gas.total_gas_spent(), 10);
513522
assert_eq!(gas.refunded(), 0);
514523
}
515524

@@ -525,7 +534,7 @@ mod tests {
525534

526535
let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
527536
assert_eq!(gas.remaining(), 90);
528-
assert_eq!(gas.spent(), 10);
537+
assert_eq!(gas.total_gas_spent(), 10);
529538
assert_eq!(gas.refunded(), 0);
530539
}
531540

@@ -545,12 +554,12 @@ mod tests {
545554

546555
let gas = call_last_frame_return(ctx.clone(), InstructionResult::Stop, ret_gas);
547556
assert_eq!(gas.remaining(), 90);
548-
assert_eq!(gas.spent(), 10);
557+
assert_eq!(gas.total_gas_spent(), 10);
549558
assert_eq!(gas.refunded(), 2); // min(20, 10/5)
550559

551560
let gas = call_last_frame_return(ctx, InstructionResult::Revert, ret_gas);
552561
assert_eq!(gas.remaining(), 90);
553-
assert_eq!(gas.spent(), 10);
562+
assert_eq!(gas.total_gas_spent(), 10);
554563
assert_eq!(gas.refunded(), 0);
555564
}
556565

@@ -566,7 +575,7 @@ mod tests {
566575
.with_cfg(CfgEnv::new_with_spec(OpSpecId::BEDROCK));
567576
let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
568577
assert_eq!(gas.remaining(), 0);
569-
assert_eq!(gas.spent(), 100);
578+
assert_eq!(gas.total_gas_spent(), 100);
570579
assert_eq!(gas.refunded(), 0);
571580
}
572581

@@ -583,7 +592,7 @@ mod tests {
583592
.with_cfg(CfgEnv::new_with_spec(OpSpecId::BEDROCK));
584593
let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
585594
assert_eq!(gas.remaining(), 100);
586-
assert_eq!(gas.spent(), 0);
595+
assert_eq!(gas.total_gas_spent(), 0);
587596
assert_eq!(gas.refunded(), 0);
588597
}
589598

src/l1block.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,9 @@ impl L1BlockInfo {
209209

210210
let operator_cost_gas_limit =
211211
self.operator_fee_charge_inner(U256::from(gas.limit()), spec_id);
212+
// Exclude reservoir gas (EIP-8037) from used gas — reservoir is unused and reimbursed.
212213
let operator_cost_gas_used = self.operator_fee_charge_inner(
213-
U256::from(gas.limit() - (gas.remaining() + gas.refunded() as u64)),
214+
U256::from(gas.limit() - (gas.remaining() + gas.reservoir() + gas.refunded() as u64)),
214215
spec_id,
215216
);
216217

0 commit comments

Comments
 (0)