Skip to content
Open
6 changes: 3 additions & 3 deletions .github/config/hive/amsterdam.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Amsterdam (BAL) hive test configuration
# Pinned from ethereum/execution-specs devnets/bal/3 @ 2026-04-14
fixtures: https://github.com/ethereum/execution-spec-tests/releases/download/bal@v5.6.1/fixtures_bal.tar.gz
eels_commit: 5c6e20abf3586f52d9e58393203ca07f2d0151fe
# Pinned from ethereum/execution-specs devnets/bal/4 @ 2026-04-21
fixtures: https://github.com/ethereum/execution-spec-tests/releases/download/sn%C3%B8bal-devnet-4%40v1.0.0/fixtures_snobal-devnet-4.tar.gz
eels_commit: 524b44617e410ab21b5122f0be5113b62a0e76ee
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ run-hive-eels-rlp: ## Run hive EELS RLP tests
run-hive-eels-blobs: ## Run hive EELS Blobs tests
$(MAKE) run-hive-eels EELS_SIM=ethereum/eels/execute-blobs

AMSTERDAM_FIXTURES_URL ?= https://github.com/ethereum/execution-spec-tests/releases/download/bal@v5.6.1/fixtures_bal.tar.gz
AMSTERDAM_FIXTURES_BRANCH ?= devnets/bal/3
AMSTERDAM_FIXTURES_URL ?= https://github.com/ethereum/execution-spec-tests/releases/download/sn%C3%B8bal-devnet-4%40v1.0.0/fixtures_snobal-devnet-4.tar.gz
AMSTERDAM_FIXTURES_BRANCH ?= devnets/bal/4
run-hive-eels-amsterdam: build-image setup-hive ## 🧪 Run hive EELS Amsterdam Engine tests
- cd hive && ./hive --client-file $(HIVE_CLIENT_FILE) --client ethrex --sim ethereum/eels/consume-engine --sim.limit ".*fork_Amsterdam.*" --sim.parallelism $(SIM_PARALLELISM) --sim.loglevel $(SIM_LOG_LEVEL) --sim.buildarg fixtures=$(AMSTERDAM_FIXTURES_URL) --sim.buildarg branch=$(AMSTERDAM_FIXTURES_BRANCH)

Expand Down
7 changes: 7 additions & 0 deletions crates/blockchain/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,10 @@ pub const MIN_GAS_LIMIT: u64 = 5000;
// === EIP-7825 constants ===
// https://eips.ethereum.org/EIPS/eip-7825
pub const POST_OSAKA_GAS_LIMIT_CAP: u64 = 16777216;

// === EIP-7981 / EIP-7976 constants (Amsterdam+) ===
// access_list_bytes * STANDARD_TOKEN_COST(4) * TOTAL_COST_FLOOR_PER_TOKEN(16) = access_list_bytes * 64
// Per address entry: 20 bytes * 64 = 1280
pub const TX_ACCESS_LIST_ADDRESS_DATA_GAS_AMSTERDAM: u64 = 1280;
// Per storage key entry: 32 bytes * 64 = 2048
pub const TX_ACCESS_LIST_STORAGE_KEY_DATA_GAS_AMSTERDAM: u64 = 2048;
25 changes: 25 additions & 0 deletions crates/blockchain/mempool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use ethrex_common::{
},
};
use ethrex_storage::error::StoreError;
use ethrex_vm::{intrinsic_gas_dimensions, intrinsic_gas_floor};
use tracing::warn;

#[derive(Debug, Default)]
Expand Down Expand Up @@ -512,6 +513,30 @@ pub fn transaction_intrinsic_gas(
header: &BlockHeader,
config: &ChainConfig,
) -> Result<u64, MempoolError> {
// Amsterdam (EIP-8037): the VM splits intrinsic into (regular, state) and uses
// `REGULAR_GAS_CREATE = 9000` + `STATE_BYTES_PER_NEW_ACCOUNT * cpsb` for CREATE
// instead of the legacy `TX_CREATE_GAS_COST = 53000`. Mempool admission must
// match VM charge or we spuriously reject (or admit) transactions.
//
// The VM enforces `gas_limit >= max(intrinsic_regular + intrinsic_state,
// floor)` via two separate checks in `validate_gas_allowance` +
// `validate_min_gas_limit`. Apply the same max here so we don't admit
// txs whose calldata floor exceeds the weighted intrinsic — those would
// pass mempool and then fail at block inclusion, polluting the pool.
if config.is_amsterdam_activated(header.timestamp) {
let fork = config.fork(header.timestamp);
let (regular, state) = intrinsic_gas_dimensions(tx, fork, header.gas_limit)
.map_err(|_| MempoolError::TxGasOverflowError)?;
let intrinsic = regular
.checked_add(state)
.ok_or(MempoolError::TxGasOverflowError)?;
let floor = intrinsic_gas_floor(tx, fork).map_err(|_| MempoolError::TxGasOverflowError)?;
// Block-level gas = max(regular_dim, state_dim); regular_dim itself is
// `max(tx_regular, calldata_floor)` per EIP-7778. Use the same max so
// admission mirrors the VM's effective minimum.
return Ok(intrinsic.max(floor));
}

let is_contract_creation = tx.is_contract_creation();

let mut gas = if is_contract_creation {
Expand Down
45 changes: 38 additions & 7 deletions crates/blockchain/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use ethrex_common::{
},
types::{
AccountUpdate, BlobsBundle, Block, BlockBody, BlockHash, BlockHeader, BlockNumber,
ChainConfig, MempoolTransaction, Receipt, Transaction, TxKind, TxType, Withdrawal,
ChainConfig, Fork, MempoolTransaction, Receipt, Transaction, TxKind, TxType, Withdrawal,
block_access_list::BlockAccessList,
bloom_from_logs, calc_excess_blob_gas, calculate_base_fee_per_blob_gas,
calculate_base_fee_per_gas, compute_receipts_root, compute_transactions_root,
Expand All @@ -26,7 +26,7 @@ use ethrex_common::{

use ethrex_crypto::NativeCrypto;
use ethrex_crypto::keccak::Keccak256;
use ethrex_vm::{Evm, EvmError};
use ethrex_vm::{Evm, EvmError, check_2d_gas_allowance};

use ethrex_rlp::encode::RLPEncode;
use ethrex_storage::{Store, error::StoreError};
Expand Down Expand Up @@ -461,8 +461,8 @@ impl Blockchain {
.chain_config()
.is_amsterdam_activated(context.payload.header.timestamp)
{
#[allow(clippy::cast_possible_truncation)]
let post_tx_index = (context.payload.body.transactions.len() + 1) as u16;
let post_tx_index =
u32::try_from(context.payload.body.transactions.len() + 1).unwrap_or(u32::MAX);
context.vm.set_bal_index(post_tx_index);
// Record withdrawal recipients as touched addresses per EIP-7928
if let Some(recorder) = context.vm.db.bal_recorder_mut()
Expand Down Expand Up @@ -650,12 +650,32 @@ impl Blockchain {
continue;
}

// EIP-8037 (Amsterdam+, PR #2703): per-tx 2D inclusion check against
// running block totals. Run BEFORE we touch the BAL recorder so a
// rejected tx doesn't even produce a sender/recipient touch. Matches
// the validator's check in `execute_block_parallel`; if we skipped
// this the builder could include txs the validator would reject,
// causing a miss-slot.
if context.is_amsterdam
&& let Err(e) = check_2d_gas_allowance(
&head_tx.tx,
Fork::Amsterdam,
context.block_regular_gas_used,
context.block_state_gas_used,
context.payload.header.gas_limit,
)
{
debug!("Skipping tx {tx_hash:x}: fails 2D inclusion check: {e}");
txs.pop();
continue;
}

// Set BAL index for this transaction (1-indexed per EIP-7928)
// Index is based on current transaction count + 1
// Must happen BEFORE tx_checkpoint: set_bal_index flushes net-zero
// filters for the previous (committed) tx, which may insert reads.
#[allow(clippy::cast_possible_truncation)]
let tx_index = (context.payload.body.transactions.len() + 1) as u16;
let tx_index =
u32::try_from(context.payload.body.transactions.len() + 1).unwrap_or(u32::MAX);
context.vm.set_bal_index(tx_index);

// EIP-7928: Lightweight tx-level checkpoint before trying the tx.
Expand Down Expand Up @@ -848,7 +868,18 @@ pub fn apply_plain_transaction(
// 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;
// `cumulative_gas_spent` was bumped inside `execute_tx` above; revert it
// now that the tx is being rejected. Use `saturating_sub` as a defensive
// guard — cumulative must always dominate this tx's contribution unless
// some upstream bug leaks a stale value, in which case we'd rather clamp
// to 0 than underflow the counter.
debug_assert!(
context.cumulative_gas_spent >= report.gas_spent,
"cumulative_gas_spent underflow on tx rollback"
);
context.cumulative_gas_spent = context
.cumulative_gas_spent
.saturating_sub(report.gas_spent);

return Err(EvmError::Custom(format!(
"block gas limit exceeded (state gas overflow): \
Expand Down
2 changes: 1 addition & 1 deletion crates/common/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub enum InvalidBlockError {
#[error("Block access list hash does not match the one in the header after executing")]
BlockAccessListHashMismatch,
#[error("Block access list contains index {index} exceeding max valid index {max}")]
BlockAccessListIndexOutOfBounds { index: u16, max: u16 },
BlockAccessListIndexOutOfBounds { index: u32, max: u32 },
#[error("Block access list exceeds gas limit, {items} items exceeds limit of {max_items}")]
BlockAccessListSizeExceeded { items: u64, max_items: u64 },
#[error("World State Root does not match the one in the header after executing")]
Expand Down
Loading
Loading