diff --git a/crates/blockchain/payload.rs b/crates/blockchain/payload.rs index c3cc0a0dc52..1748a07cc2a 100644 --- a/crates/blockchain/payload.rs +++ b/crates/blockchain/payload.rs @@ -833,10 +833,35 @@ pub fn apply_plain_transaction( // EIP-8037 (Amsterdam+): track regular and state gas separately let tx_state_gas = report.state_gas_used; let tx_regular_gas = report.gas_used.saturating_sub(tx_state_gas); - context.block_regular_gas_used = context + + // Compute new totals before committing them + let new_regular = context .block_regular_gas_used .saturating_add(tx_regular_gas); - context.block_state_gas_used = context.block_state_gas_used.saturating_add(tx_state_gas); + let new_state = context.block_state_gas_used.saturating_add(tx_state_gas); + + // EIP-8037 (Amsterdam+): post-execution block gas overflow check + // Reject the transaction if adding it would cause max(regular, state) to exceed the gas limit + if context.is_amsterdam && new_regular.max(new_state) > context.payload.header.gas_limit { + // Rollback transaction state before returning error: + // 1. Undo DB mutations (nonce, balance, storage, etc.) + // 2. Revert cumulative gas counter inflation + // This ensures the next transaction executes against clean state. + context.vm.undo_last_tx()?; + context.cumulative_gas_spent -= report.gas_spent; + + return Err(EvmError::Custom(format!( + "block gas limit exceeded (state gas overflow): \ + max({new_regular}, {new_state}) = {} > gas_limit {}", + new_regular.max(new_state), + context.payload.header.gas_limit + )) + .into()); + } + + // Commit the new totals + context.block_regular_gas_used = new_regular; + context.block_state_gas_used = new_state; if context.is_amsterdam { debug!( @@ -852,15 +877,14 @@ pub fn apply_plain_transaction( } // Update remaining_gas for block gas limit checks. - // EIP-8037 (Amsterdam+): per-tx check only validates regular gas against block limit. - // State gas is NOT checked per-tx; block-end validation enforces - // max(block_regular, block_state) <= gas_limit. + // EIP-8037 (Amsterdam+): remaining_gas reflects both regular and state gas dimensions. + // For pre-tx heuristic checks, this ensures we reject txs when either dimension is full. if context.is_amsterdam { context.remaining_gas = context .payload .header .gas_limit - .saturating_sub(context.block_regular_gas_used); + .saturating_sub(new_regular.max(new_state)); } else { context.remaining_gas = context.remaining_gas.saturating_sub(report.gas_used); } diff --git a/crates/vm/backends/levm/mod.rs b/crates/vm/backends/levm/mod.rs index 9500c2c83b7..70381b50650 100644 --- a/crates/vm/backends/levm/mod.rs +++ b/crates/vm/backends/levm/mod.rs @@ -31,7 +31,6 @@ use ethrex_levm::account::{AccountStatus, LevmAccount}; use ethrex_levm::call_frame::Stack; use ethrex_levm::constants::{ POST_OSAKA_GAS_LIMIT_CAP, STACK_LIMIT, SYS_CALL_GAS_LIMIT, TX_BASE_COST, - TX_MAX_GAS_LIMIT_AMSTERDAM, }; use ethrex_levm::db::Database; use ethrex_levm::db::gen_db::{CacheDB, GeneralizedDatabase}; @@ -126,18 +125,13 @@ impl LEVM { })?; for (tx_idx, (tx, tx_sender)) in transactions_with_sender.into_iter().enumerate() { - // Pre-tx gas limit guard per EIP-8037/EIP-7825: - // Amsterdam: check min(TX_MAX_GAS_LIMIT, tx.gas) against regular gas only. - // State gas is NOT checked per-tx; block-end validation enforces - // max(block_regular, block_state) <= gas_limit. - // Pre-Amsterdam: check tx.gas against cumulative_gas_used (post-refund sum). - if is_amsterdam { - check_gas_limit( - block_regular_gas_used, - tx.gas_limit().min(TX_MAX_GAS_LIMIT_AMSTERDAM), - block.header.gas_limit, - )?; - } else { + // Pre-tx gas limit guard: + // Pre-Amsterdam: reject tx if cumulative post-refund gas + tx.gas > block limit. + // Amsterdam+: skip — EIP-8037's 2D gas model means cumulative gas (regular + + // state) can legally exceed the block gas limit as long as + // max(sum_regular, sum_state) stays within it. Block-level overflow is + // detected post-execution. + if !is_amsterdam { check_gas_limit(cumulative_gas_used, tx.gas_limit(), block.header.gas_limit)?; } @@ -175,6 +169,20 @@ impl LEVM { report.gas_used, report.gas_spent, ); + + // DoS protection: early exit if either regular or state gas exceeds the limit. + // Since block_gas_used = max(regular, state), if either component exceeds + // the limit, we know the block is invalid and can safely reject without + // violating EIP-8037 semantics. + if block_regular_gas_used > block.header.gas_limit + || block_state_gas_used > block.header.gas_limit + { + return Err(EvmError::Transaction(format!( + "Gas allowance exceeded: Block gas used overflow: \ + block_gas_used {block_gas_used} > block_gas_limit {}", + block.header.gas_limit + ))); + } } else { block_gas_used = block_gas_used.saturating_add(report.gas_used); } @@ -189,6 +197,17 @@ impl LEVM { receipts.push(receipt); } + // EIP-7778 (Amsterdam+): block-level gas overflow check. + // Per-tx checks are skipped for Amsterdam because block gas is computed + // from pre-refund values; overflow can only be detected after execution. + if is_amsterdam && block_gas_used > block.header.gas_limit { + return Err(EvmError::Transaction(format!( + "Gas allowance exceeded: Block gas used overflow: \ + block_gas_used {block_gas_used} > block_gas_limit {}", + block.header.gas_limit + ))); + } + // Set BAL index for post-execution phase (requests + withdrawals, uint16) // Order must match geth: requests (system calls) BEFORE withdrawals. if is_amsterdam { @@ -424,18 +443,13 @@ impl LEVM { let mut tx_since_last_flush = 2; for (tx_idx, (tx, tx_sender)) in transactions_with_sender.into_iter().enumerate() { - // Pre-tx gas limit guard per EIP-8037/EIP-7825: - // Amsterdam: check min(TX_MAX_GAS_LIMIT, tx.gas) against regular gas only. - // State gas is NOT checked per-tx; block-end validation enforces - // max(block_regular, block_state) <= gas_limit. - // Pre-Amsterdam: check tx.gas against cumulative_gas_used (post-refund sum). - if is_amsterdam { - check_gas_limit( - block_regular_gas_used, - tx.gas_limit().min(TX_MAX_GAS_LIMIT_AMSTERDAM), - block.header.gas_limit, - )?; - } else { + // Pre-tx gas limit guard: + // Pre-Amsterdam: reject tx if cumulative post-refund gas + tx.gas > block limit. + // Amsterdam+: skip — EIP-8037's 2D gas model means cumulative gas (regular + + // state) can legally exceed the block gas limit as long as + // max(sum_regular, sum_state) stays within it. Block-level overflow is + // detected post-execution. + if !is_amsterdam { check_gas_limit(cumulative_gas_used, tx.gas_limit(), block.header.gas_limit)?; } @@ -483,6 +497,20 @@ impl LEVM { if is_amsterdam { // Amsterdam+: block gas = max(regular_sum, state_sum) block_gas_used = block_regular_gas_used.max(block_state_gas_used); + + // DoS protection: early exit if either regular or state gas exceeds the limit. + // Since block_gas_used = max(regular, state), if either component exceeds + // the limit, we know the block is invalid and can safely reject without + // violating EIP-8037 semantics. + if block_regular_gas_used > block.header.gas_limit + || block_state_gas_used > block.header.gas_limit + { + return Err(EvmError::Transaction(format!( + "Gas allowance exceeded: Block gas used overflow: \ + block_gas_used {block_gas_used} > block_gas_limit {}", + block.header.gas_limit + ))); + } } else { block_gas_used = block_gas_used.saturating_add(report.gas_used); } @@ -497,6 +525,17 @@ impl LEVM { receipts.push(receipt); } + // EIP-7778 (Amsterdam+): block-level gas overflow check. + // Per-tx checks are skipped for Amsterdam because block gas is computed + // from pre-refund values; overflow can only be detected after execution. + if is_amsterdam && block_gas_used > block.header.gas_limit { + return Err(EvmError::Transaction(format!( + "Gas allowance exceeded: Block gas used overflow: \ + block_gas_used {block_gas_used} > block_gas_limit {}", + block.header.gas_limit + ))); + } + #[cfg(feature = "perf_opcode_timings")] { let mut timings = OPCODE_TIMINGS.lock().expect("poison"); @@ -972,32 +1011,21 @@ impl LEVM { // balance in the BAL won't match execution that ran all txs). let mut block_regular_gas_used = 0_u64; let mut block_state_gas_used = 0_u64; - for (tx_idx, _, report, _, _, _) in &exec_results { - // Per-tx check: only regular gas is checked per-tx (EIP-8037/EIP-7825). - // State gas is validated at block end via max(regular, state) <= gas_limit. - let tx_gas_limit = txs_with_sender[*tx_idx].0.gas_limit(); - check_gas_limit( - block_regular_gas_used, - tx_gas_limit.min(TX_MAX_GAS_LIMIT_AMSTERDAM), - header.gas_limit, - )?; + for (_, _, report, _, _, _) in &exec_results { let tx_state_gas = report.state_gas_used; let tx_regular_gas = report.gas_used.saturating_sub(tx_state_gas); block_regular_gas_used = block_regular_gas_used.saturating_add(tx_regular_gas); block_state_gas_used = block_state_gas_used.saturating_add(tx_state_gas); - // Post-tx check: needed because all txs are already executed — if the last tx - // pushes actual gas over the limit, there's no next iteration to catch it - // like the sequential path does. - let running_block_gas_after = block_regular_gas_used.max(block_state_gas_used); - if running_block_gas_after > header.gas_limit { - return Err(EvmError::Transaction(format!( - "Gas allowance exceeded: \ - used {running_block_gas_after} > block limit {}", - header.gas_limit - ))); - } } let block_gas_used = block_regular_gas_used.max(block_state_gas_used); + // EIP-7778: block-level overflow check using pre-refund gas. + if block_gas_used > header.gas_limit { + return Err(EvmError::Transaction(format!( + "Gas allowance exceeded: Block gas used overflow: \ + block_gas_used {block_gas_used} > block_gas_limit {}", + header.gas_limit + ))); + } // 4. Per-tx BAL validation — now safe to run after gas limit is confirmed OK. // Also mark off storage_reads that appear in per-tx execution state.