Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions crates/blockchain/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand All @@ -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);
}
Expand Down
118 changes: 73 additions & 45 deletions crates/vm/backends/levm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
Comment on lines +130 to +133

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should still have some sort of check to prevent DoS by blocks using more gas than allowed. In principle in a 10MB block with 100B transactions each with a 16M limit we could end up executing 1.6Tgas of code before realizing the block is actually invalid.

if !is_amsterdam {
check_gas_limit(cumulative_gas_used, tx.gas_limit(), block.header.gas_limit)?;
}
Comment on lines 127 to 136

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Deferred check wastes computation during Amsterdam block production

With the per-tx guard removed, the sequential/pipeline paths execute every transaction in the block before discovering a gas overflow. For block production (as opposed to validation), this means a builder that over-fills a block will run all transactions and only learn about the overflow after the fact. A lightweight upper-bound estimate — e.g. checking whether block_regular_gas_used + tx.gas_limit() already exceeds block.header.gas_limit before executing — could short-circuit wasted work without breaking the post-refund-accounting semantics. This is a performance concern, not a correctness issue.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/vm/backends/levm/mod.rs
Line: 127-136

Comment:
**Deferred check wastes computation during Amsterdam block production**

With the per-tx guard removed, the sequential/pipeline paths execute every transaction in the block before discovering a gas overflow. For block production (as opposed to validation), this means a builder that over-fills a block will run all transactions and only learn about the overflow after the fact. A lightweight upper-bound estimate — e.g. checking whether `block_regular_gas_used + tx.gas_limit()` already exceeds `block.header.gas_limit` before executing — could short-circuit wasted work without breaking the post-refund-accounting semantics. This is a performance concern, not a correctness issue.

How can I resolve this? If you propose a fix, please make it concise.


Expand Down Expand Up @@ -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);
}
Expand All @@ -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
)));
}
Comment on lines +203 to +209

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Block-level error wrapped in EvmError::Transaction

EvmError::Transaction formats as "Invalid Transaction: …", but this error is triggered by the cumulative block-level gas total exceeding the limit — no single transaction is invalid in isolation. EvmError::Header (formats as "Invalid Header: …") would be semantically closer to a block invariant violation, or a dedicated EvmError::Block variant could be introduced. The mismatch is what drives the acknowledged remaining Hive failure where the test harness sees BlockException.GAS_USED_OVERFLOW instead of TransactionException.GAS_ALLOWANCE_EXCEEDED. The same pattern applies to the pipeline (line 503) and parallel (line 994) paths.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/vm/backends/levm/mod.rs
Line: 189-195

Comment:
**Block-level error wrapped in `EvmError::Transaction`**

`EvmError::Transaction` formats as `"Invalid Transaction: …"`, but this error is triggered by the cumulative block-level gas total exceeding the limit — no single transaction is invalid in isolation. `EvmError::Header` (formats as `"Invalid Header: …"`) would be semantically closer to a block invariant violation, or a dedicated `EvmError::Block` variant could be introduced. The mismatch is what drives the acknowledged remaining Hive failure where the test harness sees `BlockException.GAS_USED_OVERFLOW` instead of `TransactionException.GAS_ALLOWANCE_EXCEEDED`. The same pattern applies to the pipeline (line 503) and parallel (line 994) paths.

How can I resolve this? If you propose a fix, please make it concise.


// Set BAL index for post-execution phase (requests + withdrawals, uint16)
// Order must match geth: requests (system calls) BEFORE withdrawals.
if is_amsterdam {
Expand Down Expand Up @@ -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)?;
}

Expand Down Expand Up @@ -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);
}
Expand All @@ -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");
Expand Down Expand Up @@ -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.
Expand Down
Loading