diff --git a/.github/config/hive/amsterdam.yaml b/.github/config/hive/amsterdam.yaml index 64ea2b04552..ac933505e46 100644 --- a/.github/config/hive/amsterdam.yaml +++ b/.github/config/hive/amsterdam.yaml @@ -1,4 +1,4 @@ # Amsterdam (BAL) hive test configuration -# Pinned to tests-glamsterdam-devnet@v7.2.0 (execution-specs `devnets/glamsterdam/7`) -fixtures: https://github.com/ethereum/execution-specs/releases/download/tests-glamsterdam-devnet%40v7.2.0/fixtures_glamsterdam-devnet.tar.gz -eels_commit: 50a25a54997581820640a1820e9ebb4bae93486b +# Pinned to tests-glamsterdam-devnet@v8.0.0 (execution-specs `devnets/glamsterdam/8`) +fixtures: https://github.com/ethereum/execution-specs/releases/download/tests-glamsterdam-devnet%40v8.0.0/fixtures_glamsterdam-devnet.tar.gz +eels_commit: d681ca4fd019ee80099dd1899bdbee419cab8e0b diff --git a/.github/workflows/daily_hive_report.yaml b/.github/workflows/daily_hive_report.yaml index 8658213295c..db155de84e6 100644 --- a/.github/workflows/daily_hive_report.yaml +++ b/.github/workflows/daily_hive_report.yaml @@ -68,7 +68,8 @@ jobs: name: "Consume Engine tests (Amsterdam)", file_name: consume-engine-amsterdam, simulation: ethereum/eels/consume-engine, - limit: ".*fork_Amsterdam.*", + limit: ".*fork_.*Amsterdam.*", + amsterdam: true, } - { name: "Consume RLP tests (Rest)", @@ -88,7 +89,8 @@ jobs: name: "Consume RLP tests (Amsterdam)", file_name: consume-rlp-amsterdam, simulation: ethereum/eels/consume-rlp, - limit: ".*fork_Amsterdam.*", + limit: ".*fork_.*Amsterdam.*", + amsterdam: true, parallelism: 2, } - { @@ -130,6 +132,7 @@ jobs: env: SIMULATION: ${{ matrix.test.simulation }} SIM_LIMIT: ${{ matrix.test.limit || '' }} + AMSTERDAM: ${{ matrix.test.amsterdam || 'false' }} AMSTERDAM_FIXTURES: ${{ steps.amsterdam-config.outputs.fixtures }} AMSTERDAM_EELS_COMMIT: ${{ steps.amsterdam-config.outputs.eels_commit }} MAINNET_FIXTURES: ${{ steps.mainnet-config.outputs.fixtures }} @@ -137,8 +140,11 @@ jobs: run: | FLAGS="--sim.parallelism ${{ matrix.test.parallelism || 4 }} --sim.loglevel 1" if [[ "$SIMULATION" == "ethereum/eels/consume-engine" || "$SIMULATION" == "ethereum/eels/consume-rlp" ]]; then - # Use fork-specific fixtures to ensure comprehensive test coverage - if [[ "$SIM_LIMIT" == *"fork_Amsterdam"* ]]; then + # Use fork-specific fixtures to ensure comprehensive test coverage. Keyed off an + # explicit flag rather than a substring of the limit, which no longer contains a + # literal fork name; matching on the limit would fall through to the mainnet + # bundle, which carries no Amsterdam fixtures at all. + if [[ "$AMSTERDAM" == "true" ]]; then # Amsterdam config loaded from .github/config/hive/amsterdam.yaml FLAGS+=" --sim.buildarg fixtures=$AMSTERDAM_FIXTURES" FLAGS+=" --sim.buildarg branch=$AMSTERDAM_EELS_COMMIT" diff --git a/.github/workflows/pr-main_l1.yaml b/.github/workflows/pr-main_l1.yaml index 71b513528b2..73ecb029520 100644 --- a/.github/workflows/pr-main_l1.yaml +++ b/.github/workflows/pr-main_l1.yaml @@ -326,7 +326,7 @@ jobs: # Amsterdam config is loaded from .github/config/hive/amsterdam.yaml - name: "Consume Engine Amsterdam" simulation: ethereum/eels/consume-engine - limit: ".*(8024|7708|7778|7843|7928|7954|8037).*" + limit: ".*(2780|7708|7732|7778|7843|7928|7954|7975|7976|7981|7997|8024|8037|8038|8045|8061|8070|8159|8246|8282).*" amsterdam: true artifact_prefix: consume_engine_amsterdam # Investigate this test diff --git a/.gitignore b/.gitignore index a706162a4eb..5789ab693c7 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ jwt.hex /tmp *tests*.tar.gz +tooling/ef_tests/state/test.tar.gz .env diff --git a/Makefile b/Makefile index e6629f8bae6..7e0da5cd764 100644 --- a/Makefile +++ b/Makefile @@ -162,20 +162,23 @@ run-hive-eels-blobs: ## Run hive EELS Blobs tests $(MAKE) run-hive-eels EELS_SIM=ethereum/eels/execute-blobs AMSTERDAM_FIXTURES_URL ?= $(shell cat tooling/ef_tests/.fixtures_url_amsterdam) -AMSTERDAM_FIXTURES_BRANCH ?= devnets/glamsterdam/7 +AMSTERDAM_FIXTURES_BRANCH ?= devnets/glamsterdam/8 +# `fork_.*Amsterdam` rather than `fork_Amsterdam` so the BPO2->Amsterdam activation +# fixtures (`fork_BPO2ToAmsterdamAtTime15k`) are swept alongside the Amsterdam ones. +AMSTERDAM_FORK_PATTERN ?= .*fork_.*Amsterdam.* 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) + - cd hive && ./hive --client-file $(HIVE_CLIENT_FILE) --client ethrex --sim ethereum/eels/consume-engine --sim.limit "$(AMSTERDAM_FORK_PATTERN)" --sim.parallelism $(SIM_PARALLELISM) --sim.loglevel $(SIM_LOG_LEVEL) --sim.buildarg fixtures=$(AMSTERDAM_FIXTURES_URL) --sim.buildarg branch=$(AMSTERDAM_FIXTURES_BRANCH) -run-hive-eels-bal-quick: build-image setup-hive ## 🧪 Run hive EELS quick tests for the glam-7 EIPs - - cd hive && ./hive --client-file $(HIVE_CLIENT_FILE) --client ethrex --sim ethereum/eels/consume-engine --sim.limit ".*(8024|7708|7778|7843|7928|7954|8037|8038|2780|7997|7610|8246|8282).*" --sim.parallelism $(SIM_PARALLELISM) --sim.loglevel $(SIM_LOG_LEVEL) --sim.buildarg fixtures=$(AMSTERDAM_FIXTURES_URL) --sim.buildarg branch=$(AMSTERDAM_FIXTURES_BRANCH) +run-hive-eels-bal-quick: build-image setup-hive ## 🧪 Run hive EELS quick tests for the Amsterdam EIPs + - cd hive && ./hive --client-file $(HIVE_CLIENT_FILE) --client ethrex --sim ethereum/eels/consume-engine --sim.limit ".*(2780|7708|7732|7778|7843|7928|7954|7975|7976|7981|7997|8024|8037|8038|8045|8061|8070|8159|8246|8282).*" --sim.parallelism $(SIM_PARALLELISM) --sim.loglevel $(SIM_LOG_LEVEL) --sim.buildarg fixtures=$(AMSTERDAM_FIXTURES_URL) --sim.buildarg branch=$(AMSTERDAM_FIXTURES_BRANCH) # Block-building simulator (execution-specs PR #2679). Not yet upstream in Hive, # so we install the simulator Dockerfile into the hive clone and patch the # ethrex hive client to expose the `testing` namespace (testing_buildBlockV1 # lives on the public HTTP port). Defaults to the Amsterdam/BAL fixtures. -# Defaults to the BAL EIP set (mirrors run-hive-eels-bal-quick) rather than all -# .*fork_Amsterdam.* fixtures, which pull in ~21k cross-fork cases. Override with -# BUILD_BLOCK_TEST_PATTERN=.*fork_Amsterdam.* for the full sweep. +# Defaults to the EIPs whose block-building behaviour this simulator exercises, rather +# than every Amsterdam fixture, which pulls in ~21k cross-fork cases. Override with +# BUILD_BLOCK_TEST_PATTERN=$(AMSTERDAM_FORK_PATTERN) for the full sweep. BUILD_BLOCK_TEST_PATTERN ?= .*(7708|7778|7843|7928|7954|7976|7981|8024|8037).* run-hive-build-block: build-image setup-hive ## 🧱 Run hive build-block simulator (testing_buildBlockV1) mkdir -p hive/simulators/ethereum/eels/build-block diff --git a/crates/common/constants.rs b/crates/common/constants.rs index d8d45c6dc54..38278975add 100644 --- a/crates/common/constants.rs +++ b/crates/common/constants.rs @@ -80,5 +80,6 @@ pub const POST_OSAKA_GAS_LIMIT_CAP: u64 = 16777216; pub const TX_MAX_GAS_LIMIT_AMSTERDAM: u64 = 1 << 24; // 16,777,216 // === EIP-7928 BAL size cap constants === -/// GAS_BLOCK_ACCESS_LIST_ITEM = GAS_WARM_ACCESS (100) + TX_ACCESS_LIST_STORAGE_KEY_COST (1900) +/// GAS_BLOCK_ACCESS_LIST_ITEM: a flat per-item charge, independent of the +/// EIP-8038 access-list repricing. pub const BAL_ITEM_COST: u64 = 2000; diff --git a/crates/common/types/block.rs b/crates/common/types/block.rs index 627827ed856..3efd6f25351 100644 --- a/crates/common/types/block.rs +++ b/crates/common/types/block.rs @@ -920,7 +920,7 @@ pub fn calc_excess_blob_gas(parent: &BlockHeader, schedule: ForkBlobSchedule, fo mod test { use super::*; use crate::constants::EMPTY_KECCAK_HASH; - use crate::types::{BLOB_BASE_FEE_UPDATE_FRACTION, ELASTICITY_MULTIPLIER}; + use crate::types::{BLOB_BASE_FEE_UPDATE_FRACTION, ELASTICITY_MULTIPLIER, INITIAL_BASE_FEE}; use ethereum_types::H160; use hex_literal::hex; use std::str::FromStr; @@ -1144,6 +1144,37 @@ mod test { assert_eq!(res, 3538944) } + #[test] + fn test_calc_excess_blob_gas_at_amsterdam_inherits_bpo1_target() { + // Amsterdam carries no blob params of its own, so a chain that scheduled BPO1 + // and never scheduled BPO2 keeps BPO1's target of 10 and max of 15 across the + // Amsterdam boundary. A parent that spent 12 blobs is above that target, so the + // EIP-7918 reserve-price branch raises the excess by `used * (max - target) / max`. + // Resolving the schedule from an unscheduled BPO2 (target 14) instead would put + // the parent below target and wrongly return 0. + let parent = BlockHeader { + excess_blob_gas: Some(0), + blob_gas_used: Some(12 * GAS_PER_BLOB as u64), + base_fee_per_gas: Some(INITIAL_BASE_FEE), + ..Default::default() + }; + let config = ChainConfig { + osaka_time: Some(0), + bpo1_time: Some(0), + amsterdam_time: Some(100), + ..Default::default() + }; + + let schedule = config + .get_fork_blob_schedule(100) + .expect("Amsterdam must resolve a blob schedule"); + assert_eq!(schedule.target, 10); + assert_eq!(schedule.max, 15); + + let res = calc_excess_blob_gas(&parent, schedule, config.fork(100)); + assert_eq!(res, 4 * GAS_PER_BLOB as u64); + } + #[test] fn test_fake_exponential_overflow() { // With u64 this overflows diff --git a/crates/common/types/genesis.rs b/crates/common/types/genesis.rs index bad05e17b3a..16ab542251e 100644 --- a/crates/common/types/genesis.rs +++ b/crates/common/types/genesis.rs @@ -495,22 +495,16 @@ impl ChainConfig { } pub fn get_fork_blob_schedule(&self, block_timestamp: u64) -> Option { - // Hegotá inherits Amsterdam's blob schedule unless an explicit Hegotá - // entry is added to BlobSchedule in a future change. - if self.is_hegota_activated(block_timestamp) + // EIP-7892: from Prague onward the blob schedule only changes at BPO forks. + // Named forks (Osaka, Amsterdam, Hegotá) carry no blob params of their own and + // inherit the highest activated BPO entry, so resolution falls through the BPO + // chain. A genesis that pins an entry for the named fork anyway takes precedence. + if (self.is_hegota_activated(block_timestamp) + || self.is_amsterdam_activated(block_timestamp)) && let Some(schedule) = self.blob_schedule.amsterdam { return Some(schedule); } - // Amsterdam (and BPO3-5) don't independently define blob params in Hive; - // they inherit from the highest activated BPO fork. If the fork-specific - // entry is None, fall through to find the right BPO schedule. - if self.is_amsterdam_activated(block_timestamp) - && let Some(schedule) = self.blob_schedule.amsterdam - { - return Some(schedule); - } - // Fall through to BPO chain if self.is_bpo5_activated(block_timestamp) && let Some(schedule) = self.blob_schedule.bpo5 { @@ -526,8 +520,7 @@ impl ChainConfig { { return Some(schedule); } - // Amsterdam implies BPO2 blob params when no explicit schedule is set. - if self.is_bpo2_activated(block_timestamp) || self.is_amsterdam_activated(block_timestamp) { + if self.is_bpo2_activated(block_timestamp) { Some(self.blob_schedule.bpo2) } else if self.is_bpo1_activated(block_timestamp) { Some(self.blob_schedule.bpo1) @@ -635,21 +628,16 @@ impl ChainConfig { } pub fn get_blob_schedule_for_fork(&self, fork: Fork) -> Option { - match fork { - Fork::Cancun => Some(self.blob_schedule.cancun), - Fork::Prague => Some(self.blob_schedule.prague), - Fork::Osaka => Some(self.blob_schedule.osaka), - Fork::BPO1 => Some(self.blob_schedule.bpo1), - Fork::BPO2 => Some(self.blob_schedule.bpo2), - Fork::BPO3 => self.blob_schedule.bpo3, - Fork::BPO4 => self.blob_schedule.bpo4, - Fork::BPO5 => self.blob_schedule.bpo5, - Fork::Amsterdam => self.blob_schedule.amsterdam, - // Hegotá inherits Amsterdam's blob schedule unless an explicit - // Hegotá entry is added to BlobSchedule in a future change. - Fork::Hegota => self.blob_schedule.amsterdam, - _ => None, + // Blob params are timestamp-scheduled from Cancun onward; earlier forks are + // activated by block number and have no blob schedule. Resolving through the + // fork's activation timestamp keeps this in step with the inheritance rules in + // `get_fork_blob_schedule`, so a named fork that declares no entry of its own + // reports the entry actually in force at its activation. + if fork < Fork::Cancun { + return None; } + self.get_activation_timestamp_for_fork(fork) + .and_then(|timestamp| self.get_fork_blob_schedule(timestamp)) } pub fn gather_forks(&self, genesis_header: BlockHeader) -> (Vec, Vec) { @@ -1390,4 +1378,79 @@ mod tests { }; assert_eq!(config.next_fork(0), None); } + + #[test] + fn amsterdam_inherits_highest_activated_bpo_schedule() { + // EIP-7892: Amsterdam declares no blob params of its own, so a genesis that + // schedules BPO1 and then Amsterdam keeps BPO1's target and max across the + // Amsterdam boundary. An unscheduled BPO2 must never contribute its schedule. + let config: ChainConfig = serde_json::from_str( + r#"{ + "chainId": 1, + "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa", + "osakaTime": 0, + "bpo1Time": 0, + "amsterdamTime": 100, + "blobSchedule": { + "cancun": {"target": 3, "max": 6, "baseFeeUpdateFraction": 3338477}, + "prague": {"target": 6, "max": 9, "baseFeeUpdateFraction": 5007716}, + "bpo1": {"target": 10, "max": 15, "baseFeeUpdateFraction": 8346193} + } + }"#, + ) + .expect("genesis should parse"); + + let bpo1 = ForkBlobSchedule { + target: 10, + max: 15, + base_fee_update_fraction: 8346193, + }; + assert_eq!(config.get_fork_blob_schedule(99), Some(bpo1)); + assert_eq!(config.get_fork_blob_schedule(100), Some(bpo1)); + } + + #[test] + fn blob_schedule_for_fork_reports_the_inherited_entry() { + // `eth_config` (EIP-7910) must report the blob params actually in force at a + // fork's activation, which for Amsterdam is the highest activated BPO entry. + let config = ChainConfig { + osaka_time: Some(0), + bpo1_time: Some(0), + amsterdam_time: Some(100), + ..Default::default() + }; + + let schedule = config + .get_blob_schedule_for_fork(Fork::Amsterdam) + .expect("Amsterdam must report a blob schedule"); + assert_eq!((schedule.target, schedule.max), (10, 15)); + + // Forks that were never scheduled have nothing to report, and pre-Cancun forks + // are activated by block number rather than timestamp. + assert_eq!(config.get_blob_schedule_for_fork(Fork::BPO2), None); + assert_eq!(config.get_blob_schedule_for_fork(Fork::London), None); + } + + #[test] + fn amsterdam_prefers_explicitly_pinned_schedule() { + let mut config = ChainConfig { + osaka_time: Some(0), + bpo1_time: Some(0), + bpo2_time: Some(50), + amsterdam_time: Some(100), + ..Default::default() + }; + assert_eq!( + config.get_fork_blob_schedule(100), + Some(default_bpo2_schedule()) + ); + + let pinned = ForkBlobSchedule { + target: 16, + max: 24, + base_fee_update_fraction: 13353910, + }; + config.blob_schedule.amsterdam = Some(pinned); + assert_eq!(config.get_fork_blob_schedule(100), Some(pinned)); + } } diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs index f2d8b9ae99c..b27f3055967 100644 --- a/crates/networking/rpc/engine/payload.rs +++ b/crates/networking/rpc/engine/payload.rs @@ -935,6 +935,19 @@ fn parse_execution_payload(params: &Option>) -> Result Result<(), RpcErr> { + if payload.block_access_list.is_some() { + return Err(RpcErr::WrongParam("block_access_list".to_string())); + } + if payload.slot_number.is_some() { + return Err(RpcErr::WrongParam("slot_number".to_string())); + } + + Ok(()) +} + fn validate_execution_payload_v1(payload: &ExecutionPayload) -> Result<(), RpcErr> { // Validate that only the required arguments are present if payload.withdrawals.is_some() { @@ -947,7 +960,7 @@ fn validate_execution_payload_v1(payload: &ExecutionPayload) -> Result<(), RpcEr return Err(RpcErr::WrongParam("excess_blob_gas".to_string())); } - Ok(()) + reject_amsterdam_payload_fields(payload) } fn validate_execution_payload_v2(payload: &ExecutionPayload) -> Result<(), RpcErr> { @@ -962,11 +975,11 @@ fn validate_execution_payload_v2(payload: &ExecutionPayload) -> Result<(), RpcEr return Err(RpcErr::WrongParam("excess_blob_gas".to_string())); } - Ok(()) + reject_amsterdam_payload_fields(payload) } -fn validate_execution_payload_v3(payload: &ExecutionPayload) -> Result<(), RpcErr> { - // Validate that only the required arguments are present +/// Fields shared by every payload version from Cancun onwards. +fn validate_execution_payload_cancun_fields(payload: &ExecutionPayload) -> Result<(), RpcErr> { if payload.withdrawals.is_none() { return Err(RpcErr::WrongParam("withdrawals".to_string())); } @@ -980,16 +993,25 @@ fn validate_execution_payload_v3(payload: &ExecutionPayload) -> Result<(), RpcEr Ok(()) } +/// Shared by `engine_newPayloadV3` and `engine_newPayloadV4`, both of which predate Amsterdam. +fn validate_execution_payload_v3(payload: &ExecutionPayload) -> Result<(), RpcErr> { + // Validate that only the required arguments are present + validate_execution_payload_cancun_fields(payload)?; + + reject_amsterdam_payload_fields(payload) +} + #[inline] fn validate_execution_payload_v4(payload: &ExecutionPayload) -> Result<(), RpcErr> { - // This method follows the same specification as `engine_newPayloadV4` additionally - // rejects payload without block access list + // The Amsterdam payload shape: the Cancun fields plus a block access list. Reached only + // through `validate_execution_payload_v5`, so the Amsterdam fields are required here + // rather than rejected. if payload.block_access_list.is_none() { return Err(RpcErr::WrongParam("block_access_list".to_string())); } - validate_execution_payload_v3(payload)?; + validate_execution_payload_cancun_fields(payload)?; Ok(()) } diff --git a/crates/vm/levm/src/gas_cost.rs b/crates/vm/levm/src/gas_cost.rs index cb1a829dae0..5779be057dd 100644 --- a/crates/vm/levm/src/gas_cost.rs +++ b/crates/vm/levm/src/gas_cost.rs @@ -210,23 +210,29 @@ pub const BLOB_GAS_PER_BLOB: u64 = 131072; pub const ACCESS_LIST_STORAGE_KEY_COST: u64 = 1900; pub const ACCESS_LIST_ADDRESS_COST: u64 = 2400; -// ===== EIP-8038 Amsterdam values (merged EIPs#11802) ===== +// ===== EIP-8038 Amsterdam values ===== pub const COLD_ACCOUNT_ACCESS_AMSTERDAM: u64 = 3000; -pub const COLD_STORAGE_ACCESS_AMSTERDAM: u64 = 3000; -pub const ACCESS_LIST_ADDRESS_COST_AMSTERDAM: u64 = 3000; -pub const ACCESS_LIST_STORAGE_KEY_COST_AMSTERDAM: u64 = 3000; +pub const COLD_STORAGE_ACCESS_AMSTERDAM: u64 = 2100; +// Prepaying an access-list entry is gas neutral with the cold access it replaces: +// the entry costs the cold charge minus the WARM_ACCESS the later touch still pays. +// EIP-2930's extra 100 discount is deliberately not restored. +pub const ACCESS_LIST_ADDRESS_COST_AMSTERDAM: u64 = + COLD_ACCOUNT_ACCESS_AMSTERDAM - WARM_ADDRESS_ACCESS_COST; +pub const ACCESS_LIST_STORAGE_KEY_COST_AMSTERDAM: u64 = + COLD_STORAGE_ACCESS_AMSTERDAM - WARM_ADDRESS_ACCESS_COST; pub const STORAGE_WRITE_AMSTERDAM: u64 = 10000; -pub const ACCOUNT_WRITE_AMSTERDAM: u64 = 8000; -pub const CALL_VALUE_AMSTERDAM: u64 = 10300; -pub const STORAGE_CLEAR_REFUND_AMSTERDAM: i64 = 12480; -pub const CREATE_ACCESS_AMSTERDAM: u64 = 11000; +pub const ACCOUNT_WRITE_AMSTERDAM: u64 = 9000; +pub const CALL_VALUE_AMSTERDAM: u64 = ACCOUNT_WRITE_AMSTERDAM + CALL_POSITIVE_VALUE_STIPEND; +pub const STORAGE_CLEAR_REFUND_AMSTERDAM: i64 = + ((STORAGE_WRITE_AMSTERDAM + COLD_STORAGE_ACCESS_AMSTERDAM) * 4800 / 5000) as i64; +pub const CREATE_ACCESS_AMSTERDAM: u64 = ACCOUNT_WRITE_AMSTERDAM + COLD_ACCOUNT_ACCESS_AMSTERDAM; -// ===== EIP-2780 Amsterdam values (merged EIPs#11645) ===== +// ===== EIP-2780 Amsterdam values ===== // Resource-based intrinsic transaction gas. The flat 21000 base is decomposed // into: sender base (TX_BASE_COST_AMSTERDAM = 12000), recipient access, and a -// value-transfer charge split between a transfer log cost and a value cost. -pub const TX_VALUE_COST_AMSTERDAM: u64 = 4244; -pub const TRANSFER_LOG_COST_AMSTERDAM: u64 = 1756; +// value-transfer charge covering both the recipient balance write and the +// EIP-7708 transfer log. +pub const TX_VALUE_COST_AMSTERDAM: u64 = 6000; // EIP-8038: size in bytes of one RLP-encoded authorization tuple, used to // derive its calldata-floor contribution below. @@ -269,7 +275,7 @@ pub fn cold_storage_access_cost(fork: Fork) -> u64 { } } -/// Per-address access-list cost. EIP-8038 raises this from 2400 to 3000 at Amsterdam. +/// Per-address access-list cost. EIP-8038 raises this from 2400 to 2900 at Amsterdam. pub fn access_list_address_cost(fork: Fork) -> u64 { if fork >= Fork::Amsterdam { ACCESS_LIST_ADDRESS_COST_AMSTERDAM @@ -278,7 +284,7 @@ pub fn access_list_address_cost(fork: Fork) -> u64 { } } -/// Per-storage-key access-list cost. EIP-8038 raises this from 1900 to 3000 at Amsterdam. +/// Per-storage-key access-list cost. EIP-8038 raises this from 1900 to 2900 at Amsterdam. pub fn access_list_storage_key_cost(fork: Fork) -> u64 { if fork >= Fork::Amsterdam { ACCESS_LIST_STORAGE_KEY_COST_AMSTERDAM @@ -288,7 +294,7 @@ pub fn access_list_storage_key_cost(fork: Fork) -> u64 { } /// Upfront positive-value cost for CALL / CALLCODE. EIP-8038 raises this from -/// 9000 to 10300 (`CALL_VALUE_AMSTERDAM`) at Amsterdam. This is the charge +/// 9000 to `CALL_VALUE_AMSTERDAM` at Amsterdam. This is the charge /// applied to the *caller* before the call; it is NOT the 2300 stipend /// (`CALL_POSITIVE_VALUE_STIPEND`) forwarded to the callee, which is unchanged. pub fn call_positive_value_cost(fork: Fork) -> u64 { @@ -315,25 +321,20 @@ pub fn recipient_regular_gas(to: &TxKind, value: U256, sender: Address, fork: Fo return 0; } - let is_create = matches!(to, TxKind::Create); - let regular_gas = if is_create { - CREATE_ACCESS_AMSTERDAM - } else { - cold_account_access_cost(fork) - }; + // A contract creation charges no value cost: the recipient balance write is + // already covered by CREATE_ACCESS. + if matches!(to, TxKind::Create) { + return CREATE_ACCESS_AMSTERDAM; + } #[expect( clippy::arithmetic_side_effects, - reason = "sum of small constant gas costs (<= ~17000), cannot overflow u64" + reason = "sum of small constant gas costs (<= ~9000), cannot overflow u64" )] - if !value.is_zero() { - if is_create { - regular_gas + TRANSFER_LOG_COST_AMSTERDAM - } else { - regular_gas + TRANSFER_LOG_COST_AMSTERDAM + TX_VALUE_COST_AMSTERDAM - } + if value.is_zero() { + cold_account_access_cost(fork) } else { - regular_gas + cold_account_access_cost(fork) + TX_VALUE_COST_AMSTERDAM } } @@ -722,8 +723,8 @@ fn compute_gas_create( 0 }; - // EIP-8038: CREATE/CREATE2 opcode regular base is CREATE_ACCESS (11000); - // the new-account leaf is charged separately in state gas. + // EIP-8038: CREATE/CREATE2 opcode regular base is CREATE_ACCESS; the + // new-account leaf is charged separately in state gas. let create_base_cost = if fork >= Fork::Amsterdam { CREATE_ACCESS_AMSTERDAM } else { diff --git a/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs b/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs index 0e05cdb706a..931e0bed2be 100644 --- a/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs +++ b/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs @@ -284,9 +284,9 @@ impl OpcodeHandler for OpSStoreHandler { }; if fork >= Fork::Amsterdam { - // EIP-8038 (review CRITICAL #2) raises COLD_STORAGE_ACCESS to 3000, - // above the 2300 stipend, so the flat pre-Amsterdam gate above no - // longer suffices as the EIP-2200 sentry. Peek warmth WITHOUT + // EIP-8038 prices the cold storage access independently of the 2300 + // stipend, so the flat pre-Amsterdam gate above no longer suffices + // as the EIP-2200 sentry. Peek warmth WITHOUT // marking the slot accessed, gate on // `max(access_cost, SSTORE_STIPEND + 1)` per EELS `check_gas` // (amsterdam/vm/instructions/storage.py::sstore), and only mark the @@ -358,7 +358,7 @@ impl OpcodeHandler for OpSStoreHandler { // state gas is handled via the reservoir, not these regular deltas. let (remove_slot_cost, restore_empty_slot_cost, restore_slot_cost): (i64, i64, i64) = if fork >= Fork::Amsterdam { - // Amsterdam: clear refund 12480; both restore deltas are the full STORAGE_WRITE. + // Amsterdam: both restore deltas are the full STORAGE_WRITE. (STORAGE_CLEAR_REFUND_AMSTERDAM, 10000, 10000) } else { // EIP-2929 diff --git a/crates/vm/levm/src/utils.rs b/crates/vm/levm/src/utils.rs index c8093767eff..4f2dbf64d30 100644 --- a/crates/vm/levm/src/utils.rs +++ b/crates/vm/levm/src/utils.rs @@ -329,7 +329,7 @@ impl<'a> VM<'a> { /// atomic prepare region (so an OOG rolls the whole region back and burns all gas /// rather than rejecting the tx). Per valid authorization, in EELS order: /// - `NEW_ACCOUNT` (state) when the authority's account leaf does not yet exist; - /// - `ACCOUNT_WRITE` (regular, 8000) when this is the transaction's first write to + /// - `ACCOUNT_WRITE` (regular) when this is the transaction's first write to /// the authority's leaf (the sender's leaf was written at inclusion, and the /// recipient's when `value > 0`, so those pay nothing here — a self-sponsored /// authority and repeated authorizations on one authority pay `ACCOUNT_WRITE` at diff --git a/docs/developers/l1/testing/hive.md b/docs/developers/l1/testing/hive.md index 8d56bbdd770..e2e7739baef 100644 --- a/docs/developers/l1/testing/hive.md +++ b/docs/developers/l1/testing/hive.md @@ -9,7 +9,7 @@ This project uses three key repositories for Hive testing: 1. **[ethereum/hive](https://github.com/ethereum/hive)** - The main Hive testing framework - Current commit: `0921fb7833e3de180eacdc9f26de6e51dcab0dba` 2. **[ethereum/execution-specs](https://github.com/ethereum/execution-specs)** - Test fixtures and vectors (the former `ethereum/execution-spec-tests` repo is archived) - - Mainnet: `tests@v20.0.0` (Osaka + BPO1 + BPO2); Amsterdam: `tests-glamsterdam-devnet@v7.2.0` + - Mainnet: `tests@v20.0.0` (Osaka + BPO1 + BPO2); Amsterdam: `tests-glamsterdam-devnet@v8.0.0` 3. **[ethereum/execution-specs](https://github.com/ethereum/execution-specs)** - Fork specifications - Current branch: `forks/amsterdam` @@ -314,7 +314,7 @@ Contents: https://github.com/ethereum/execution-specs/releases/download/tests%40v20.0.0/fixtures.tar.gz # .fixtures_url_amsterdam -https://github.com/ethereum/execution-specs/releases/download/tests-glamsterdam-devnet%40v7.2.0/fixtures_glamsterdam-devnet.tar.gz +https://github.com/ethereum/execution-specs/releases/download/tests-glamsterdam-devnet%40v8.0.0/fixtures_glamsterdam-devnet.tar.gz ``` The CI hive config lives in `.github/config/hive/{mainnet,amsterdam}.yaml`, each pinning a `fixtures` URL and an `eels_commit` (the execution-specs commit used to build the hive consumer). diff --git a/docs/known_issues.md b/docs/known_issues.md index f4f4f09f698..4a131094b8f 100644 --- a/docs/known_issues.md +++ b/docs/known_issues.md @@ -5,18 +5,16 @@ fixtures with `network >= Fork::Amsterdam` when running with a stateless backend Affects `make test-stateless` (the `vectors_zkevm/` run); `make test-levm` is unaffected. -**Why:** The stateless run uses the `tests-zkevm@v0.5.0` bundle, filled against -`glamsterdam-devnet` v6.1.0, which predeploys the EIP-8282 builder deposit/exit -contracts at the OLD addresses (`0x0000884d…d9008282` / `0x000014574a…0f008282`). -This client uses the devnet-7 addresses (`0x0000bff4…300d8282` / -`0x000064d6…800e8282`, matching the live `tests-glamsterdam-devnet@v7.2.0` bundle -used by `make test-levm`). Every Amsterdam+ block runs the end-of-block EIP-8282 -builder system call; with the new addresses absent from the v0.5.0 bundle, each -stateless Amsterdam+ block fails with -`SystemContractCallFailed("System contract: 0x0000…8282 has no code after deployment")`. -The skip is by fork rather than by test name, since cross-fork directories such as -`for_amsterdam/prague/...` still execute at the Amsterdam fork. +**Why:** the stateless run uses the `tests-zkevm@v0.6.2` bundle, the newest zkEVM +release, filled against `tests-glamsterdam-devnet@v7.2.0`. This client targets +`glamsterdam-devnet-8`, whose gas schedule diverges from devnet-7: EIP-2780 folds the +EIP-7708 transfer log cost into `TX_VALUE_COST`, and EIP-8038 reprices access-list +entries to the cold cost minus `WARM_ACCESS` (3000 → 2900 per address and per storage +key). Every Amsterdam+ fixture in the bundle therefore carries devnet-7 gas +expectations that no longer match execution. The skip is by fork rather than by test +name, since cross-fork directories such as `for_amsterdam/prague/...` still execute at +the Amsterdam fork. -**Removal:** Delete the `skip_stateless_amsterdam` branch in `parse_and_execute` -once a `tests-zkevm` bundle filled with the devnet-7 builder predeploy addresses is -released and `.fixtures_url_zkevm` is bumped to it. +**Removal:** delete the `skip_stateless_amsterdam` branch in `parse_and_execute` once a +`tests-zkevm` bundle filled against `glamsterdam-devnet-8` is released and +`.fixtures_url_zkevm` is bumped to it. diff --git a/docs/roadmaps/forks-roadmap.md b/docs/roadmaps/forks-roadmap.md index 3de0d40be45..153e3e3761d 100644 --- a/docs/roadmaps/forks-roadmap.md +++ b/docs/roadmaps/forks-roadmap.md @@ -1,233 +1,147 @@ # Forks Team Roadmap - ethrex -## Amsterdam / Glamsterdam → Mainnet June 2026 - -## Glossary +Next fork: **Glamsterdam** (CL Gloas, EL Amsterdam). Mainnet date not yet scheduled. | Acronym | Meaning | |---------|---------| -| **SFI** | Scheduled for Inclusion - Will be in the fork | -| **CFI** | Considered for Inclusion - Likely, under discussion | -| **DFI** | Declined for Inclusion - Won't be included | -| **PFI** | Proposed for Inclusion - Proposed | +| **SFI** | Scheduled for Inclusion - will be in the fork | +| **CFI** | Considered for Inclusion - likely, under discussion | +| **PFI** | Proposed for Inclusion - proposed | +| **DFI** | Declined for Inclusion - won't be included | | **BAL** | Block-Level Access Lists (EIP-7928) | --- ## Current Devnet -**bal-devnet-7** — last `bal-`-prefixed devnet. Future devnets prefixed `glamsterdam-`. +**glamsterdam-devnet-8** -- Spec baseline: [`devnets/bal/7`](https://github.com/ethereum/execution-specs/tree/devnets/bal/7) -- Fixtures: [`tests-bal@v7.2.0`](https://github.com/ethereum/execution-specs/releases/tag/tests-bal@v7.2.0) (`.github/config/hive/amsterdam.yaml`) -- EELS commit: `a3e5201a53d8c94e2283ae170a2c71bbc233f7e7` -- Status: 🟢 aligned — blockchain ef-tests + hive `eels/consume-engine` Amsterdam all passing +- Spec baseline: [`devnets/glamsterdam/8`](https://github.com/ethereum/execution-specs/tree/devnets/glamsterdam/8) +- Fixtures: [`tests-glamsterdam-devnet@v8.0.0`](https://github.com/ethereum/execution-specs/releases/tag/tests-glamsterdam-devnet@v8.0.0) +- EELS commit: `d681ca4fd019ee80099dd1899bdbee419cab8e0b` +- Status: 🟢 aligned — blockchain, state and engine ef-tests green on the v8.0.0 bundle - Tracking: [#6583] ---- - -## Implementation Status - -### Implemented — Amsterdam EL (per [EIP-7773]) - -| EIP | Title | Status | SFI/CFI | Owner | -|-----|-------|--------|---------|-------| -| **7928** | Block-Level Access Lists | ✅ Implemented (devnet-7 aligned) | SFI (EL headliner) | Edgar | -| **7708** | ETH Transfers Emit Logs | ✅ Implemented | SFI | Edgar | -| **7778** | Block Gas Accounting without Refunds | ✅ Implemented | SFI | Edgar | -| **7843** | SLOTNUM Opcode | ✅ Implemented | SFI | Esteve | -| **8024** | DUPN/SWAPN/EXCHANGE | ✅ Implemented | SFI | Esteve | -| **8037** | State Creation Gas Cost (2D gas) | ✅ Implemented (devnet-7 aligned) | SFI (CFI→SFI @ ACDE #236) | Edgar | -| **7976** | Increase Calldata Floor Cost | ✅ Implemented | SFI | | -| **7981** | Increase Access List Cost | ✅ Implemented | SFI | | -| **7954** | Increase Max Contract Size (24→32 KiB) | ✅ Implemented | SFI | | -| **8159** | eth/71 Block Access List Exchange | ✅ Implemented | SFI (protocol req for bal-7) | | -| **7872** | Max Blob Flag for Local Builders | ✅ Implemented | PFI | Edgar | -| **8025** | Optional Execution Proofs | ✅ Implemented ([#6361], #6516, #6549, #6560) | Hegotá PFI ([EIP-8081]) | | - -> **8025 note:** Stagnant on eips.ethereum.org and listed as PFI for Hegotá in [EIP-8081]. ethrex code paths exist (zkboost SSZ); status may shift to Hegotá-only. - -### Not Implemented — Amsterdam EL candidates - -Per [EIP-7773]: - -| EIP | Title | SFI/CFI | Notes | -|-----|-------|---------|-------| -| **2780** | Reduce Intrinsic Transaction Gas (21000→4500) | CFI | No other client started | -| **7904** | General Repricing | CFI | Nethermind draft #9619 only | -| **8038** | State-Access Gas Cost Update | CFI | No other client started | -| **7997** | Deterministic Factory Predeploy | CFI | No other client started | -| **8070** | Sparse Blobpool | CFI | No other client started | -| **7610** | Revert Creation on Non-empty Storage | PFI | Confirmed PFI in [EIP-7773] | -| **7979** | Call/Return Opcodes | PFI | | -| **8163** | Reserve Opcode | PFI | | +**Bumping to a new bundle:** edit `tooling/ef_tests/.fixtures_url_amsterdam` and +`.github/config/hive/amsterdam.yaml` (`fixtures` + `eels_commit`), then re-run the three +ef-test suites and hive `eels/consume-engine` Amsterdam. Upstream expects at least two +follow-up releases on this devnet (v8.1.0, v8.2.0) carrying coverage rather than new +semantics. -### Amsterdam EL CFI candidates (not yet evaluated) +## Next up -Not in ethrex; need triage per ACDE outcomes: - -| EIP | Title | SFI/CFI | -|-----|-------|---------| -| **7688** | Consensus structures | CFI | -| **7975** | Networking | CFI | -| **8045** | Core | CFI | -| **8061** | Core | CFI | -| **8080** | Core | CFI | -| **8136** | Networking | CFI | -| **8246** | Core | CFI | - -### CL-side (informational) - -| EIP | Title | SFI/CFI | -|-----|-------|---------| -| **7732** | Enshrined Proposer-Builder Separation (ePBS) | SFI (CL headliner) | - -### Notable DFI - -Declined from Glamsterdam per [EIP-7773]: 47 EIPs including **EIP-7805 (FOCIL)**, EIP-7692 (EOF), EIP-7937 (64-bit EVM). FOCIL re-targeted at Hegotá. +| Item | Why it matters | +|------|----------------| +| **EIP-8070 (eth/72)** | Mandatory for all ELs on devnet-8. In review at [#6776]. Ships no fixtures and hive covers it execute-only ([hive#1365]), so the bundle gives it zero coverage either way — needs an `execute` sim wired up. | +| **`debug_getRawBlockAccessList`** | Protocol-side requirement per [execution-apis#794](https://github.com/ethereum/execution-apis/pull/794), along with `-32001` for the BAL getters. In review at [#7069]. | +| **devnet-8 zkEVM bundle** | `tests-zkevm` is still filled against devnet-7, so the stateless run skips every Amsterdam+ fixture (see [known issues](../known_issues.md)). No devnet-8 stateless coverage until a new bundle ships. | +| **EIP-8038 spec text** | The v8.0.0 access-list repricing landed in the tests ahead of its EIPs PR. Confirm the EIP matches once that merges. | +| **`eth_simulateV1`** | Still unimplemented. Tracked at [#6212]. | +| **EIP-8189 (snap/2)** | BAL-based state healing, newly listed in [EIP-7773]. Not evaluated. | +| **EIP-7904** | Informational compute-gas analysis; only a Nethermind draft ([#9619]) exists. Revisit if it reaches SFI. | --- -## Active Work - -### `tests-bal@v7.3.0` (expected ~2026-05-29) +## Implementation Status -Stability + extra tests only; no new spec semantics. Bundled upstream PRs: +### Amsterdam EL — devnet-8 scope (16 EIPs per [EIP-7773]) -**EIP-8037:** -- [specs#2898](https://github.com/ethereum/execution-specs/pull/2898) — reject when `calldata_floor > TX_MAX_GAS_LIMIT` -- [specs#2892](https://github.com/ethereum/execution-specs/pull/2892) — strict block-gas inclusion rule (**spec gap**; audit ethrex EIP-8037 block-gas inclusion against the strict rule before bumping the fixture pin) -- [specs#2876](https://github.com/ethereum/execution-specs/pull/2876) — reject tx when `gas_limit` covers regular but not state intrinsic -- [specs#2875](https://github.com/ethereum/execution-specs/pull/2875) — CREATE-tx collision refunds state-gas reservoir +15 implemented, EIP-8070 in review. The gas EIPs repriced in v8.0.0: -**EIP-7928:** -- [specs#2897](https://github.com/ethereum/execution-specs/pull/2897) — extend BAL coverage -- [specs#2883](https://github.com/ethereum/execution-specs/pull/2883) — BAL withdrawal predeploy balance read across txs (Edgar) -- [specs#2893](https://github.com/ethereum/execution-specs/pull/2893) — selfdestruct to system address with 0 value +| EIP | Title | devnet-8 pricing | Owner | +|-----|-------|------------------|-------| +| **2780** | Resource-based Intrinsic Transaction Gas | transfer log cost folded into a flat `TX_VALUE_COST` of 6000; a creation carrying value pays no value charge | Edgar | +| **8038** | State-Access Gas Cost Update | access-list entries cost the cold access minus `WARM_ACCESS` (2900), so prepaying is gas neutral | Edgar | +| **8037** | State Creation Gas Cost (2D gas) | flat two-dimensional inclusion gate; only the execution dimension is capped at `TX_MAX_GAS_LIMIT` | Edgar | +| **7928** | Block-Level Access Lists | unchanged in v8.0.0 | Edgar | -**Action on drop:** bump `.github/config/hive/amsterdam.yaml` `fixtures`/`eels_commit`, re-run blockchain ef-tests + hive `eels/consume-engine` Amsterdam. +Also implemented and passing: **7708** ETH transfers emit logs (Edgar), **7778** block gas +accounting without refunds (Edgar), **7843** SLOTNUM (Esteve), **8024** +DUPN/SWAPN/EXCHANGE (Esteve), **7976** calldata floor cost, **7981** access list cost, +**7954** max contract size (24→32 KiB), **7610** revert creation on non-empty storage, +**8246** remove SELFDESTRUCT burn, **8282** builder execution requests, **7997** +deterministic factory (genesis predeploy, no client code). -### [EIPs#11699] — EIP-7702 delegation BAL exclusion +Outside the devnet-8 set: **8159** eth/71 BAL exchange, **7975** eth/70 partial receipt +lists, **7872** max blob flag (PFI), **8025** optional execution proofs ([#6361], #6516, +#6549, #6560 — Hegotá PFI per [EIP-8081], may end up Hegotá-only). -Tightens EIP-7928 §"EIP-7702 Delegation" so the delegated address is added to the BAL only if all of: -1. Sufficient gas for delegated `access_cost` -2. For value-transferring `CALL`/`CALLCODE`, `sender_balance >= value` -3. Call stack depth not violated +### Not implemented — EL candidates -ethrex currently matches the **old** spec. When EELS merges: -- Move delegation `code_address` BAL recording from `record_bal_call_touch` (`crates/vm/levm/src/opcode_handlers/system.rs:889`) to after the `sender_balance`/depth guards inside `generic_call` (~line 962). -- Update `test/tests/levm/eip7928_tests.rs` to cover: 7702 + insufficient balance, 7702 + max depth. -- EELS fixtures will rewrite `test_bal_call_revert_insufficient_funds` for the 4 `delegated-*` variants. +| EIP | Title | Stage | +|-----|-------|-------| +| **7904** | Compute Gas Cost Analysis | CFI (Informational) | +| **7979** | Call/Return Opcodes | PFI | +| **8163** | Reserve Opcode | PFI | +| **8189** | snap/2 BAL-Based State Healing | listed in [EIP-7773], not evaluated | -### `eth_simulateV1` RPC +### CL-side -Not implemented. Tracked at [#6212]. +No EL work; tracked so ACDE outcomes are visible. **7732** ePBS (SFI, CL headliner), +**7688** forward-compatible consensus structures, **8045** exclude slashed validators from +proposing, **8061** exit/consolidation churn, **8080** exits via the consolidation queue, +**8136** cell-level deltas for data column broadcast. ---- +Glamsterdam declined 47 EIPs per [EIP-7773], including EIP-7692 (EOF) and EIP-7937 +(64-bit EVM). EIP-7805 (FOCIL) was re-targeted at Hegotá. -## Out of Scope / Deferred +### Would reopen if re-proposed -- **`debug_getRawBlockAccessList` RPC + `-32001` error code** per [execution-apis#794](https://github.com/ethereum/execution-apis/pull/794) — required for bal-devnet-7 protocol-side; tracked separately. -- **Debug receipt fields** ([PM #2033](https://github.com/ethereum/pm/issues/2033#issuecomment-4397074196)) — qu0b polling clients on extending `debug_getBlockReceipts` with `regularGasUsed` / `stateGasCharged` / `stateGasRefunded` / `cumulative*`. Cross-client debug aid; not bal-7 scope. -- **Deferred-on-success state-gas charging** for `CREATE`/`CREATE2`/`CALL*` (misilva73 audit point #3 in [specs#2804](https://github.com/ethereum/execution-specs/issues/2804)) — not landing in bal-7 per Maria Silva on Discord 2026-05-08. -- **EIP-8025 zkboost fixtures** — RESOLVED: the stateless harness now tracks `tests-zkevm@v0.6.2` (filled against glamsterdam-devnet v7.2.0, matching the live Amsterdam bundle); all fixtures run with no blanket or per-fork skip and pass. -- **Remaining gas repricing EIPs** (2780, 7904, 8038) — no other client has started; revisit if SFI'd at ACDE. +- **Deferred-on-success state-gas charging** for `CREATE`/`CREATE2`/`CALL*` — misilva73 + audit point #3 in [specs#2804](https://github.com/ethereum/execution-specs/issues/2804), + declined for the BAL devnets. +- **Debug receipt fields** — extending `debug_getBlockReceipts` with `regularGasUsed` / + `stateGasCharged` / `stateGasRefunded` / `cumulative*` + ([PM #2033](https://github.com/ethereum/pm/issues/2033#issuecomment-4397074196)). A + cross-client debug aid, not fork scope. --- ## Fork Infrastructure -`crates/common/types/genesis.rs` — fork enum order: - -``` -Frontier, FrontierThawing, Homestead, DaoFork, Tangerine, SpuriousDragon, -Byzantium, Constantinople, Petersburg, Istanbul, MuirGlacier, Berlin, -London, ArrowGlacier, GrayGlacier, Paris, Shanghai, Cancun, Prague, -Osaka, BPO1, BPO2, BPO3, BPO4, BPO5, Amsterdam -``` - -Activation timestamps wired in `ChainConfig`: `shanghai_time`, `cancun_time`, `prague_time`, `osaka_time`, `bpo1_time`..`bpo5_time`, `amsterdam_time`, plus `verkle_time`. - -Network configs with Amsterdam timestamps: -- `cmd/ethrex/networks/holesky/genesis.json` -- `cmd/ethrex/networks/sepolia/genesis.json` -- `cmd/ethrex/networks/hoodi/genesis.json` - -Docker: `bal-devnet-7` not in [`ethpandaops/eth-client-docker-image-builder/branches.yaml`](https://github.com/ethpandaops/eth-client-docker-image-builder/blob/master/branches.yaml); `ethpandaops/ethrex:bal-devnet-7` images update via manual Discord `workflow_dispatch`. +The fork enum lives in `crates/common/types/genesis.rs`, ending at `Amsterdam`; +activation timestamps are `ChainConfig` fields, with `bpo1_time`..`bpo5_time` already +defined for the BPOs and Hegotá. Amsterdam timestamps are wired into the holesky, sepolia +and hoodi genesis files under `cmd/ethrex/networks/`. --- ## Next Fork: Hegotá (H2 2026) -Post-Glamsterdam fork. CL = **Heka**, EL = **Bogotá** (some secondary press uses "Heze/Hegota"; primary source: [EIP-8081]). - -### SFI - -| EIP | Title | Notes | -|-----|-------|-------| -| **7805** | FOCIL — Fork-choice enforced Inclusion Lists | **Headliner.** Promoted to SFI after DFI from Glamsterdam | - -### CFI - -| EIP | Title | Notes | -|-----|-------|-------| -| **8141** | Frame Transaction (Account Abstraction) | Lost headliner debate; retained as non-headliner CFI | - -### PFI - -| EIP | Title | -|-----|-------| -| **4758** | Deactivate `SELFDESTRUCT` | -| **7709** | Read `BLOCKHASH` from storage and update cost (presented ACDE #236) | -| **7716** | Anti-correlation attestation penalties | -| **8025** | Optional Execution Proofs (ethrex already has code paths; see Amsterdam table) | -| **8188** | State Tiering by Write Age | -| **8205** | Withdrawal credentials preregistration | -| **8253** | Bump nonce of zero-nonce storage accounts (presented ACDE #236) | +CL = **Heka**, EL = **Bogotá**; primary source [EIP-8081]. -### Infrastructure - -`bpo1_time`..`bpo5_time` already defined in `ChainConfig` (see Fork Infrastructure above). +- **SFI:** **7805** FOCIL — headliner, promoted after being declined from Glamsterdam. +- **CFI:** **8141** Frame Transaction (account abstraction) — lost the headliner debate, + retained as a non-headliner. +- **PFI:** **4758** deactivate `SELFDESTRUCT`, **7709** read `BLOCKHASH` from storage, + **7716** anti-correlation attestation penalties, **8025** optional execution proofs, + **8188** state tiering by write age, **8205** withdrawal credentials preregistration, + **8253** bump nonce of zero-nonce storage accounts. --- -## ACDE Follow-up - -Meetings on **Thursdays**. Agendas/notes at [ethereum/pm](https://github.com/ethereum/pm). Options: +## Meetings -1. **Attend live** — direct participation -2. **Post-call review** — YouTube + transcript with Claude: - - Timestamps for specific topics - - Summary of EIP discussions - - Track CFI/SFI status changes +- **ACDE** — Thursdays. Attend live, or review the recording and transcript afterwards for + CFI/SFI status changes. +- **ACDT** (testing) — Mondays, followed by Edgar. -## ACDT Follow-up - -All Core Devs — Testing meetings on **Mondays**. Agendas/notes at [ethereum/pm](https://github.com/ethereum/pm). Followed by Edgar. - ---- +Agendas and notes for both: [ethereum/pm](https://github.com/ethereum/pm). ## Links -- [EIP-7773 Meta Glamsterdam][EIP-7773] -- [EIP-8081 Meta Hegotá (Heka/Bogotá)][EIP-8081] -- [EIP-7928 Block-Level Access Lists](https://eips.ethereum.org/EIPS/eip-7928) -- [EIP-7732 ePBS (Glamsterdam CL headliner)](https://eips.ethereum.org/EIPS/eip-7732) -- [EIP-7805 FOCIL (Hegotá SFI)](https://eips.ethereum.org/EIPS/eip-7805) -- [Ansgar — Glamsterdam EL PFI'd EIPs](https://notes.ethereum.org/@ansgar/glamsterdam-el-pfi-eips) -- [ACDE #236 — May 7 2026](https://github.com/ethereum/pm/issues/2033) -- [qu0b's bal-devnet-7 spec sheet](https://gist.github.com/qu0b/f3f905cadee4464a1a941838a5a5fadb) +- [EIP-7773 Meta Glamsterdam][EIP-7773] · [EIP-8081 Meta Hegotá][EIP-8081] - [Upstream tracker — execution-specs#2804](https://github.com/ethereum/execution-specs/issues/2804) +- [Reth Amsterdam tracking issue](https://github.com/paradigmxyz/reth/issues/18783) - [ethrex docs/eip.md](../eip.md) — EIP tracking - [ethrex ROADMAP.md](../../ROADMAP.md) — general roadmap -### Other Client References -- [Nethermind PR #9619](https://github.com/NethermindEth/nethermind/pull/9619) — EIP-7904 General Repricing (Draft) -- [Reth Issue #18783](https://github.com/paradigmxyz/reth/issues/18783) — Amsterdam Hardfork Tracking - +[#9619]: https://github.com/NethermindEth/nethermind/pull/9619 +[hive#1365]: https://github.com/ethereum/hive/pull/1365 [#6212]: https://github.com/lambdaclass/ethrex/issues/6212 [#6361]: https://github.com/lambdaclass/ethrex/pull/6361 [#6583]: https://github.com/lambdaclass/ethrex/issues/6583 -[EIPs#11699]: https://github.com/ethereum/EIPs/pull/11699 +[#6776]: https://github.com/lambdaclass/ethrex/pull/6776 +[#7069]: https://github.com/lambdaclass/ethrex/pull/7069 [EIP-7773]: https://eips.ethereum.org/EIPS/eip-7773 [EIP-8081]: https://eips.ethereum.org/EIPS/eip-8081 diff --git a/fixtures/networks/glamsterdam-devnet-8-light.yaml b/fixtures/networks/glamsterdam-devnet-8-light.yaml new file mode 100644 index 00000000000..ed4cd8698e3 --- /dev/null +++ b/fixtures/networks/glamsterdam-devnet-8-light.yaml @@ -0,0 +1,38 @@ +participants: + - el_type: geth + el_image: ethpandaops/geth:glamsterdam-devnet-8 + cl_type: lodestar + cl_image: ethpandaops/lodestar:glamsterdam-devnet-8 + - el_type: ethrex + el_image: ethrex:local + cl_type: grandine + cl_image: ethpandaops/grandine:glamsterdam-devnet-8-minimal + el_extra_env_vars: + RUST_LOG: "debug,ethrex_p2p::tx_broadcaster=info" + +global_log_level: debug +network_params: + preset: minimal + deploy_eip8282_contracts: true + gloas_fork_epoch: 1 + gas_limit: 600000000 + genesis_gaslimit: 600000000 + withdrawal_type: "0x02" + +additional_services: + - spamoor + +# No BPO2 is scheduled, so Amsterdam inherits BPO1's target of 10 and max of 15. +# Clients that disagree on the target only reject each other's headers when a +# parent block's blob gas lands between the two targets, so keep blob load in +# that range rather than saturating at max. +spamoor_params: + image: ethpandaops/spamoor:latest + spammers: + - scenario: blobs + config: + throughput: 8 + max_pending: 20 + - scenario: eoatx + config: + throughput: 15 diff --git a/fixtures/networks/glamsterdam-devnet-8.yaml b/fixtures/networks/glamsterdam-devnet-8.yaml new file mode 100644 index 00000000000..7b4bdeb0d2e --- /dev/null +++ b/fixtures/networks/glamsterdam-devnet-8.yaml @@ -0,0 +1,61 @@ +participants: + - el_type: geth + el_image: ethpandaops/geth:glamsterdam-devnet-8 + cl_type: lodestar + cl_image: ethpandaops/lodestar:glamsterdam-devnet-8 + - el_type: besu + el_image: ethpandaops/besu:glamsterdam-devnet-8 + cl_type: teku + cl_image: ethpandaops/teku:glamsterdam-devnet-8 + - el_type: nethermind + el_image: ethpandaops/nethermind:glamsterdam-devnet-8 + cl_type: teku + cl_image: ethpandaops/teku:glamsterdam-devnet-8 + - el_type: reth + el_image: ethpandaops/reth:glamsterdam-devnet-8 + cl_type: grandine + cl_image: ethpandaops/grandine:glamsterdam-devnet-8-minimal + - el_type: erigon + el_image: ethpandaops/erigon:glamsterdam-devnet-8 + cl_type: lodestar + cl_image: ethpandaops/lodestar:glamsterdam-devnet-8 + - el_type: ethrex + el_image: ethrex:local + cl_type: grandine + cl_image: ethpandaops/grandine:glamsterdam-devnet-8-minimal + +global_log_level: debug +network_params: + preset: minimal + deploy_eip8282_contracts: true + gloas_fork_epoch: 1 + gas_limit: 600000000 + genesis_gaslimit: 600000000 + withdrawal_type: "0x02" + +additional_services: + - dora + - spamoor + - assertoor + - otel + - buildoor + +buildoor_params: + image: ethpandaops/buildoor:main + builder_api: true + epbs_builder: true + lifecycle: false + instances: + - participant: 1 + - participant: 3 + +assertoor_params: + run_stability_check: false + run_block_proposal_check: false + tests: + - file: "https://raw.githubusercontent.com/ethpandaops/assertoor/3f8697c64823f9fe0aaeed8065df0a4a412843a5/playbooks/gloas-dev/builder-prefork-queuefill.yaml" + config: + builderCount: 1 + builderKeyIndex: 0 + builderMnemonic: "test test test test test test test test test test test junk" + junkCount: 20 diff --git a/test/tests/blockchain/eip8037_block_gate_tests.rs b/test/tests/blockchain/eip8037_block_gate_tests.rs new file mode 100644 index 00000000000..b0131132f9b --- /dev/null +++ b/test/tests/blockchain/eip8037_block_gate_tests.rs @@ -0,0 +1,174 @@ +//! EIP-8037 two-dimensional block inclusion gate. +//! +//! The gate is flat: a tx's worst-case contribution to each dimension is measured +//! against that dimension's remaining budget with no credit for what the tx will +//! actually spend. Specifically, no intrinsic gas is subtracted and no allowance is +//! made for the state gas the tx charges at its top frame. A client that credited +//! either would accept transactions the spec rejects. +//! +//! Only the execution dimension is capped at `TX_MAX_GAS_LIMIT`; the state dimension +//! measures the full `tx.gas`. + +use ethrex_common::{ + Address, U256, + constants::TX_MAX_GAS_LIMIT_AMSTERDAM, + types::{EIP1559Transaction, Transaction, TxKind}, +}; +use ethrex_vm::check_2d_gas_allowance; + +/// A tx carrying no calldata, no access list and no value, so its only relevant +/// property is `gas_limit`. +fn tx_with_gas(gas_limit: u64) -> Transaction { + Transaction::EIP1559Transaction(EIP1559Transaction { + chain_id: 1, + nonce: 0, + max_priority_fee_per_gas: 0, + max_fee_per_gas: 0, + gas_limit, + to: TxKind::Call(Address::from_low_u64_be(0xBEEF)), + value: U256::zero(), + ..Default::default() + }) +} + +const BLOCK_GAS_LIMIT: u64 = 30_000_000; + +/// Execution gas already consumed, chosen so the remaining budget +/// ([`SUB_CAP_AVAILABLE`]) sits below `TX_MAX_GAS_LIMIT`. Above the cap the +/// execution contribution is clamped and the clamp, not the flat gate, would decide +/// these cases. +const SUB_CAP_USED: u64 = 25_000_000; +const SUB_CAP_AVAILABLE: u64 = BLOCK_GAS_LIMIT - SUB_CAP_USED; + +#[test] +fn test_execution_dim_exact_fit_is_accepted() { + // tx.gas exactly equals the remaining execution budget: the control case, valid + // under both a flat gate and one that credits the intrinsic or state charge. + check_2d_gas_allowance( + &tx_with_gas(SUB_CAP_AVAILABLE), + SUB_CAP_USED, + 0, + BLOCK_GAS_LIMIT, + ) + .expect("a tx whose gas exactly fills the remaining budget must be accepted"); +} + +#[test] +fn test_execution_dim_one_above_is_rejected() { + // One gas above the remaining budget. A client subtracting the intrinsic cost + // would have slack here and wrongly accept. + let err = check_2d_gas_allowance( + &tx_with_gas(SUB_CAP_AVAILABLE + 1), + SUB_CAP_USED, + 0, + BLOCK_GAS_LIMIT, + ) + .expect_err("one gas over the remaining execution budget must be rejected"); + assert!( + err.to_string().contains("regular dim"), + "expected the execution dimension to reject, got: {err}" + ); +} + +#[test] +fn test_execution_dim_no_credit_for_top_frame_state_charge() { + // The gate gives no credit for state gas the tx will charge at its top frame, + // so a tx over by any amount is rejected even when the state budget is untouched. + // 11_000 is CREATE_ACCESS, the state charge a creation pays at its top frame. + for over_by in [1, 1_000, 11_000] { + assert!( + check_2d_gas_allowance( + &tx_with_gas(SUB_CAP_AVAILABLE + over_by), + SUB_CAP_USED, + 0, + BLOCK_GAS_LIMIT + ) + .is_err(), + "over by {over_by} must be rejected regardless of the untouched state budget" + ); + } +} + +#[test] +fn test_execution_dim_contribution_is_capped_at_tx_max_gas_limit() { + // Only the execution dimension is capped: a tx asking for more than + // TX_MAX_GAS_LIMIT contributes just the cap, so it fits a budget smaller than + // its own gas limit. + let block_gas_limit = TX_MAX_GAS_LIMIT_AMSTERDAM * 2; + let tx_gas = TX_MAX_GAS_LIMIT_AMSTERDAM + 5_000_000; + // Leave exactly the cap free in the execution dimension. + let used = block_gas_limit - TX_MAX_GAS_LIMIT_AMSTERDAM; + check_2d_gas_allowance(&tx_with_gas(tx_gas), used, 0, block_gas_limit) + .expect("the execution contribution is capped at TX_MAX_GAS_LIMIT, so this fits"); + + // One gas less available and the capped contribution no longer fits. + check_2d_gas_allowance(&tx_with_gas(tx_gas), used + 1, 0, block_gas_limit) + .expect_err("the capped contribution must still be measured against the budget"); +} + +#[test] +fn test_state_dim_uses_full_tx_gas_uncapped() { + // The state dimension is not capped, so the same over-cap tx is rejected on the + // state side once the state budget drops below the full tx.gas. + let block_gas_limit = TX_MAX_GAS_LIMIT_AMSTERDAM * 2; + let tx_gas = TX_MAX_GAS_LIMIT_AMSTERDAM + 5_000_000; + let state_used = block_gas_limit - tx_gas + 1; + let err = check_2d_gas_allowance(&tx_with_gas(tx_gas), 0, state_used, block_gas_limit) + .expect_err("the state dimension measures the full tx.gas, uncapped"); + assert!( + err.to_string().contains("state dim"), + "expected the state dimension to reject, got: {err}" + ); +} + +#[test] +fn test_state_dim_exact_fit_is_accepted() { + let tx_gas = 5_000_000; + let state_used = BLOCK_GAS_LIMIT - tx_gas; + check_2d_gas_allowance(&tx_with_gas(tx_gas), 0, state_used, BLOCK_GAS_LIMIT) + .expect("a tx whose gas exactly fills the remaining state budget must be accepted"); +} + +#[test] +fn test_dimensions_are_tracked_independently() { + // A tx can fit the execution budget while overflowing the state budget, which is + // the whole point of tracking two dimensions against one block gas limit. + let tx_gas = 10_000_000; + check_2d_gas_allowance(&tx_with_gas(tx_gas), 20_000_000, 0, BLOCK_GAS_LIMIT) + .expect("execution budget exactly fits"); + check_2d_gas_allowance(&tx_with_gas(tx_gas), 0, 20_000_000, BLOCK_GAS_LIMIT) + .expect("state budget exactly fits"); + assert!( + check_2d_gas_allowance(&tx_with_gas(tx_gas), 20_000_001, 0, BLOCK_GAS_LIMIT).is_err(), + "execution dimension must reject on its own" + ); + assert!( + check_2d_gas_allowance(&tx_with_gas(tx_gas), 0, 20_000_001, BLOCK_GAS_LIMIT).is_err(), + "state dimension must reject on its own" + ); +} + +#[test] +fn test_exhausted_budget_rejects_any_nonzero_tx() { + // Saturating arithmetic must not turn an already-overfull block into free space. + assert!( + check_2d_gas_allowance( + &tx_with_gas(21_000), + BLOCK_GAS_LIMIT + 1, + 0, + BLOCK_GAS_LIMIT + ) + .is_err(), + "an over-limit execution total leaves no budget" + ); + assert!( + check_2d_gas_allowance( + &tx_with_gas(21_000), + 0, + BLOCK_GAS_LIMIT + 1, + BLOCK_GAS_LIMIT + ) + .is_err(), + "an over-limit state total leaves no budget" + ); +} diff --git a/test/tests/blockchain/mod.rs b/test/tests/blockchain/mod.rs index 9abd2529195..6d77215b840 100644 --- a/test/tests/blockchain/mod.rs +++ b/test/tests/blockchain/mod.rs @@ -5,6 +5,7 @@ mod batch_tests; mod canonical_commit_gate_tests; mod eip7702_revert_authority_tests; mod eip7702_zero_transfer_tests; +mod eip8037_block_gate_tests; mod explicit_blob_tx_tests; mod l1_tx_type_tests; mod logs_bloom_tests; diff --git a/test/tests/levm/eip2780_tests.rs b/test/tests/levm/eip2780_tests.rs index 281df17fe2f..1fedae81920 100644 --- a/test/tests/levm/eip2780_tests.rs +++ b/test/tests/levm/eip2780_tests.rs @@ -1,4 +1,4 @@ -//! EIP-2780 (PRELIMINARY EIPs#11645) resource-based intrinsic transaction gas. +//! EIP-2780 resource-based intrinsic transaction gas. //! //! At Amsterdam the flat 21000 intrinsic base is decomposed into resource-based //! charges: @@ -8,9 +8,8 @@ //! * contract-creation: CREATE_ACCESS_AMSTERDAM = 11000 regular + new-account state gas //! * else: cold_account_access_cost = 3000 //! - value transfer: -//! * zero value or self-transfer: 0 -//! * non-zero value contract-creation: TRANSFER_LOG_COST_AMSTERDAM = 1756 -//! * else: TRANSFER_LOG_COST_AMSTERDAM + TX_VALUE_COST_AMSTERDAM = 1756 + 4244 = 6000 +//! * zero value, self-transfer, or contract-creation: 0 +//! * else: TX_VALUE_COST_AMSTERDAM = 6000 //! //! These tests assert the intrinsic regular-gas decomposition at Amsterdam, the //! pre-Amsterdam (Osaka) control (byte-identical 21000-base), parity between @@ -43,12 +42,11 @@ use once_cell::sync::OnceCell; use rustc_hash::FxHashMap; use std::sync::Arc; -// Resource-based constants under test (PRELIMINARY EIPs#11645). +// Resource-based constants under test. const TX_BASE_COST_AMSTERDAM: u64 = 12000; const CREATE_ACCESS_AMSTERDAM: u64 = 11000; const COLD_ACCOUNT_ACCESS_AMSTERDAM: u64 = 3000; -const TRANSFER_LOG_COST_AMSTERDAM: u64 = 1756; -const TX_VALUE_COST_AMSTERDAM: u64 = 4244; +const TX_VALUE_COST_AMSTERDAM: u64 = 6000; // Pre-Amsterdam base for the Osaka control. const TX_BASE_COST: u64 = 21000; const CREATE_BASE_COST: u64 = 32000; @@ -206,7 +204,7 @@ fn test_intrinsic_zero_value_to_account_amsterdam() { #[test] fn test_intrinsic_eth_transfer_to_existing_eoa_amsterdam() { - // non-zero value to a distinct account: base + cold access + transfer log + value. + // non-zero value to a distinct account: base + cold access + value cost. let tx = call_tx( TxKind::Call(Address::from_low_u64_be(0xBEEF)), U256::from(1u64), @@ -214,11 +212,8 @@ fn test_intrinsic_eth_transfer_to_existing_eoa_amsterdam() { let (regular, state) = intrinsic_with_parity(Fork::Amsterdam, &tx); assert_eq!( regular, - TX_BASE_COST_AMSTERDAM - + COLD_ACCOUNT_ACCESS_AMSTERDAM - + TRANSFER_LOG_COST_AMSTERDAM - + TX_VALUE_COST_AMSTERDAM, - "ETH transfer regular gas (12000 + 3000 + 1756 + 4244 = 21000)" + TX_BASE_COST_AMSTERDAM + COLD_ACCOUNT_ACCESS_AMSTERDAM + TX_VALUE_COST_AMSTERDAM, + "ETH transfer regular gas (12000 + 3000 + 6000 = 21000)" ); assert_eq!( regular, 21000, @@ -250,16 +245,17 @@ fn test_intrinsic_create_zero_value_amsterdam() { #[test] fn test_intrinsic_create_nonzero_value_amsterdam() { - // contract-creation, value>0: base + CREATE_ACCESS + transfer log (no value cost). + // contract-creation, value>0: base + CREATE_ACCESS. No value charge — the recipient + // balance write is already covered by CREATE_ACCESS. // As above, the NEW_ACCOUNT state gas is charged in-region, not at intrinsic time. let tx = call_tx(TxKind::Create, U256::from(1u64)); let (regular, state) = intrinsic_with_parity(Fork::Amsterdam, &tx); assert_eq!( regular, - TX_BASE_COST_AMSTERDAM + CREATE_ACCESS_AMSTERDAM + TRANSFER_LOG_COST_AMSTERDAM, - "create value>0 regular gas (23000 + 1756 = 24756)" + TX_BASE_COST_AMSTERDAM + CREATE_ACCESS_AMSTERDAM, + "create value>0 regular gas (12000 + 11000 = 23000)" ); - assert_eq!(regular, 24756, "create value>0 regular gas must be 24756"); + assert_eq!(regular, 23000, "create value>0 regular gas must be 23000"); assert_eq!( state, 0, "create intrinsic state gas is 0 (NEW_ACCOUNT moved in-region)" diff --git a/test/tests/levm/eip8037_tests.rs b/test/tests/levm/eip8037_tests.rs index b336a838f8d..1edfe158d70 100644 --- a/test/tests/levm/eip8037_tests.rs +++ b/test/tests/levm/eip8037_tests.rs @@ -21,7 +21,7 @@ use ethrex_common::{ }; use ethrex_crypto::NativeCrypto; use ethrex_levm::{ - constants::SET_CODE_DELEGATION_BYTES, + constants::{SET_CODE_DELEGATION_BYTES, TX_MAX_GAS_LIMIT_AMSTERDAM}, db::{Database, gen_db::GeneralizedDatabase}, environment::{EVMConfig, Environment}, errors::DatabaseError, @@ -30,7 +30,7 @@ use ethrex_levm::{ cost_per_state_byte, }, tracing::LevmCallTracer, - utils::intrinsic_gas_dimensions, + utils::{intrinsic_gas_dimensions, intrinsic_gas_floor}, vm::{VM, VMType}, }; use ethrex_rlp::encode::RLPEncode; @@ -2123,3 +2123,84 @@ fn test_set_delegation_self_sponsored_no_account_write_amsterdam() { "non-self authority pays +8000 ACCOUNT_WRITE that a self-sponsored one does not" ); } + +// ===== EIP-8037 per-tx execution-gas cap ===== + +/// Runs a tx through validation only, returning the error if it is rejected. +/// +/// The tx is sent to a bare `EXEC_CONTRACT` (empty code), so anything that comes +/// back is a validation verdict rather than an execution outcome. +fn validate_tx(fork: Fork, tx: &Transaction) -> Result<(), String> { + let mut env = exec_env(fork); + env.gas_limit = tx.gas_limit(); + let mut db = exec_db(vec![], false, &[]); + let mut vm = VM::new( + env, + &mut db, + tx, + LevmCallTracer::disabled(), + VMType::L1, + &NativeCrypto, + ) + .expect("VM::new"); + vm.execute().map(|_| ()).map_err(|e| e.to_string()) +} + +/// Builds a CALL tx whose calldata is `len` non-zero bytes. +fn calldata_tx(len: usize, gas_limit: u64) -> Transaction { + Transaction::EIP1559Transaction(EIP1559Transaction { + chain_id: 1, + nonce: 0, + max_priority_fee_per_gas: 0, + max_fee_per_gas: 0, + gas_limit, + to: TxKind::Call(EXEC_CONTRACT), + value: U256::zero(), + data: Bytes::from(vec![0xFFu8; len]), + access_list: Default::default(), + ..Default::default() + }) +} + +#[test] +fn test_calldata_floor_above_tx_max_gas_limit_is_rejected() { + // A non-zero calldata byte is 4 tokens and the Amsterdam floor is 16 per token, + // so the floor grows at 64 gas per byte. Past TX_MAX_GAS_LIMIT / 64 bytes the + // floor alone exceeds the per-tx execution-gas cap, and no gas_limit can rescue + // the tx: excess gas_limit becomes state-gas reservoir, not execution gas. + let bytes_at_cap = (TX_MAX_GAS_LIMIT_AMSTERDAM / 64) as usize; + let tx = calldata_tx(bytes_at_cap + 1_000, BLOCK_GAS_LIMIT); + + // The generous gas_limit clears the floor check, so a rejection can only come + // from the cap. + let floor = intrinsic_gas_floor(&tx, EXEC_SENDER, Fork::Amsterdam).expect("floor"); + assert!( + floor > TX_MAX_GAS_LIMIT_AMSTERDAM, + "fixture must put the floor above the cap, got {floor}" + ); + assert!( + tx.gas_limit() > floor, + "fixture must fund the floor so the floor check cannot fire" + ); + + let err = validate_tx(Fork::Amsterdam, &tx) + .expect_err("a calldata floor above TX_MAX_GAS_LIMIT must be rejected"); + assert!( + err.contains("gas limit lower than the minimum gas cost"), + "expected the IntrinsicGasTooLow arm, got: {err}" + ); +} + +#[test] +fn test_calldata_floor_just_below_tx_max_gas_limit_is_accepted() { + // The control: the same shape with a floor under the cap validates, so the + // rejection above is the cap and not the calldata volume. + let bytes_at_cap = (TX_MAX_GAS_LIMIT_AMSTERDAM / 64) as usize; + let tx = calldata_tx(bytes_at_cap - 10_000, BLOCK_GAS_LIMIT); + let floor = intrinsic_gas_floor(&tx, EXEC_SENDER, Fork::Amsterdam).expect("floor"); + assert!( + floor < TX_MAX_GAS_LIMIT_AMSTERDAM, + "fixture must keep the floor under the cap, got {floor}" + ); + validate_tx(Fork::Amsterdam, &tx).expect("a floor under the cap must be accepted"); +} diff --git a/test/tests/levm/eip8038_tests.rs b/test/tests/levm/eip8038_tests.rs index ea304fe1895..7501a0b883d 100644 --- a/test/tests/levm/eip8038_tests.rs +++ b/test/tests/levm/eip8038_tests.rs @@ -110,10 +110,11 @@ fn test_warm_account_access_unchanged_across_forks() { #[test] fn test_access_list_selectors() { // The fork selectors expose the repriced per-entry constants directly. - assert_eq!(gas_cost::access_list_address_cost(Fork::Amsterdam), 3000); + // Each is the cold access minus WARM_ACCESS, so prepaying is gas neutral. + assert_eq!(gas_cost::access_list_address_cost(Fork::Amsterdam), 2900); assert_eq!( gas_cost::access_list_storage_key_cost(Fork::Amsterdam), - 3000 + 2900 ); // Osaka controls: legacy literals. assert_eq!(gas_cost::access_list_address_cost(Fork::Osaka), 2400); @@ -161,8 +162,8 @@ fn test_access_list_intrinsic_per_address_amsterdam() { let eip7981_addr_data = 20 * 4 * 16; // bytes * STANDARD_TOKEN_COST * floor(16) assert_eq!( with_addr - base - eip7981_addr_data, - 3000, - "per-address access-list constant at Amsterdam must be 3000" + 2900, + "per-address access-list constant at Amsterdam must be 2900" ); } @@ -192,8 +193,8 @@ fn test_access_list_intrinsic_per_key_amsterdam() { let eip7981_key_data = 32 * 4 * 16; assert_eq!( one_key - zero_keys - eip7981_key_data, - 3000, - "per-key access-list constant at Amsterdam must be 3000" + 2900, + "per-key access-list constant at Amsterdam must be 2900" ); } diff --git a/tooling/ef_tests/.fixtures_url_amsterdam b/tooling/ef_tests/.fixtures_url_amsterdam index 1f9e2d41a8e..57b28a62ecb 100644 --- a/tooling/ef_tests/.fixtures_url_amsterdam +++ b/tooling/ef_tests/.fixtures_url_amsterdam @@ -1 +1 @@ -https://github.com/ethereum/execution-specs/releases/download/tests-glamsterdam-devnet%40v7.2.0/fixtures_glamsterdam-devnet.tar.gz +https://github.com/ethereum/execution-specs/releases/download/tests-glamsterdam-devnet%40v8.1.0/fixtures_glamsterdam-devnet.tar.gz diff --git a/tooling/ef_tests/blockchain/Makefile b/tooling/ef_tests/blockchain/Makefile index 95f8faa16b1..a22364e85d5 100644 --- a/tooling/ef_tests/blockchain/Makefile +++ b/tooling/ef_tests/blockchain/Makefile @@ -16,7 +16,14 @@ AMSTERDAM_FIXTURES_FILE := ../.fixtures_url_amsterdam AMSTERDAM_ARTIFACT := amsterdam-tests.tar.gz AMSTERDAM_URL := $(shell cat $(AMSTERDAM_FIXTURES_FILE)) -# zkevm@v0.6.2 is filled against glamsterdam-devnet v7.2.0 (the same base as the +# Subtrees the Amsterdam bundle contributes. `for_bpo2toamsterdamattime15k` holds the +# fork-activation fixtures (a header field appearing mid-chain, the gas schedule +# switching between blocks); it exists only in this bundle, so omitting it leaves the +# whole fork boundary untested. +AMSTERDAM_SUBTREES := for_amsterdam for_bpo2toamsterdamattime15k +AMSTERDAM_STAMP := $(SPECTEST_VECTORS_DIR)/.amsterdam_overlay + +# zkevm@v0.6.2 is filled against glamsterdam-devnet v7.2.0 (one devnet behind the # Amsterdam bundle) but additionally carries the EIP-8025 stateless witness # fields (executionWitness / statelessInputBytes / statelessOutputBytes). Both # bundles extract a `for_amsterdam/` subtree, so we keep the zkevm bundle in a @@ -49,13 +56,22 @@ $(LEGACYTEST_VECTORS_DIR): $(LEGACYTEST_ARTIFACT) $(LEGACYTEST_ARCHIVE_ROOT)/GeneralStateTests \ $(LEGACYTEST_ARCHIVE_ROOT)/ValidBlocks -$(AMSTERDAM_ARTIFACT): $(AMSTERDAM_FIXTURES_FILE) +# Order-only on $(SPECTEST_VECTORS_DIR): building it runs clean-vectors, which deletes +# $(AMSTERDAM_ARTIFACT). Downloading afterwards keeps the bundle from being wiped +# between the curl and the extract. +$(AMSTERDAM_ARTIFACT): $(AMSTERDAM_FIXTURES_FILE) | $(SPECTEST_VECTORS_DIR) curl -L -o $(AMSTERDAM_ARTIFACT) $(AMSTERDAM_URL) -$(SPECTEST_VECTORS_DIR)/for_amsterdam: $(AMSTERDAM_ARTIFACT) $(SPECTEST_VECTORS_DIR) - tar -xzf $(AMSTERDAM_ARTIFACT) --strip-components=2 -C $(SPECTEST_VECTORS_DIR) fixtures/blockchain_tests/for_amsterdam +# Clear the subtrees first: the bundle is overlaid onto an existing `vectors/eest`, so +# fixtures renamed or dropped between devnet releases would otherwise survive as stale +# leftovers and run against the current client. +$(AMSTERDAM_STAMP): $(AMSTERDAM_ARTIFACT) $(SPECTEST_VECTORS_DIR) + rm -rf $(addprefix $(SPECTEST_VECTORS_DIR)/,$(AMSTERDAM_SUBTREES)) + tar -xzf $(AMSTERDAM_ARTIFACT) --strip-components=2 -C $(SPECTEST_VECTORS_DIR) \ + $(addprefix fixtures/blockchain_tests/,$(AMSTERDAM_SUBTREES)) + touch $@ -amsterdam-vectors: $(SPECTEST_VECTORS_DIR)/for_amsterdam +amsterdam-vectors: $(AMSTERDAM_STAMP) $(ZKEVM_ARTIFACT): $(ZKEVM_FIXTURES_FILE) curl -L -o $(ZKEVM_ARTIFACT) $(ZKEVM_URL) diff --git a/tooling/ef_tests/blockchain/test_runner.rs b/tooling/ef_tests/blockchain/test_runner.rs index 1638eb91c5f..d1f3ec9b672 100644 --- a/tooling/ef_tests/blockchain/test_runner.rs +++ b/tooling/ef_tests/blockchain/test_runner.rs @@ -56,15 +56,16 @@ pub fn parse_and_execute( let mut failures = Vec::new(); for (test_key, test) in tests { - // TEMPORARY: the stateless run uses the tests-zkevm@v0.5.0 bundle (filled - // against glamsterdam-devnet v6.1.0), which predeploys the EIP-8282 builder - // deposit/exit contracts at the OLD addresses. This client uses the devnet-7 - // addresses, so every Amsterdam+ block's end-of-block builder system call - // finds no code at the new addresses and fails. Skip Amsterdam+ fixtures in - // the stateless run — by fork, not by name, since cross-fork dirs like - // `for_amsterdam/prague/...` still run at the Amsterdam fork — until a zkevm - // bundle filled with the new predeploy addresses is released and - // `.fixtures_url_zkevm` is bumped. See docs/known_issues.md. + // TEMPORARY: the stateless run uses the tests-zkevm@v0.6.2 bundle, filled + // against glamsterdam-devnet v7.2.0, while this client targets devnet-8. The + // devnet-8 gas schedule differs (EIP-2780 folds the transfer log cost into + // TX_VALUE_COST, EIP-8038 reprices access-list entries to cold minus + // WARM_ACCESS), so every Amsterdam+ fixture in the bundle expects gas this + // client no longer charges. Skip Amsterdam+ fixtures in the stateless run — + // by fork, not by name, since cross-fork dirs like `for_amsterdam/prague/...` + // still run at the Amsterdam fork — until a zkevm bundle filled against + // devnet-8 is released and `.fixtures_url_zkevm` is bumped. + // See docs/known_issues.md. let skip_stateless_amsterdam = stateless_backend.is_some() && test.network >= Fork::Amsterdam; let should_skip_test = test.network < Fork::Merge diff --git a/tooling/ef_tests/blockchain/tests/all.rs b/tooling/ef_tests/blockchain/tests/all.rs index 1f0ce85be56..e14a7d5f762 100644 --- a/tooling/ef_tests/blockchain/tests/all.rs +++ b/tooling/ef_tests/blockchain/tests/all.rs @@ -35,17 +35,13 @@ const EXTRA_SKIPS: &[&str] = &[ "static_Call1MB1024Calldepth", ]; // The stateless run executes the zkevm@v0.6.2 bundle (`vectors_zkevm/`), filled against -// `tests-glamsterdam-devnet@v7.2.0` — the same base as the live `vectors/` fixtures on this -// branch. v0.6.2 fixes the EIP-8282 fill (PR ethereum/execution-specs#3157): the canonical -// `SszExecutionRequests` now carries the builder-deposit (0x03) and builder-exit (0x04) request -// lists, mirrored in `eip8025_ssz::ExecutionRequests`. The whole bundle re-executes cleanly, so -// no blanket skip and no per-fork skip are needed. Per-fixture leniency cases -// (`*_extra_unused_*` padding, deliberately-invalid witnesses) are handled in `test_runner.rs`. -// Amsterdam+ fixtures are skipped in the stateless run by fork (see -// `parse_and_execute` in `test_runner.rs` and docs/known_issues.md): the -// tests-zkevm@v0.5.0 bundle predeploys the EIP-8282 builder contracts at the OLD -// addresses, incompatible with this client's devnet-7 addresses. That skip is -// fork-based (not name-based), so no per-test entries are needed here. +// `tests-glamsterdam-devnet@v7.2.0` — one devnet behind the live `vectors/` fixtures on this +// branch. Amsterdam+ fixtures are skipped in the stateless run by fork (see +// `parse_and_execute` in `test_runner.rs` and docs/known_issues.md), because the devnet-8 +// gas schedule this client implements no longer matches the bundle's devnet-7 expectations. +// That skip is fork-based (not name-based), so no per-test entries are needed here. +// Per-fixture leniency cases (`*_extra_unused_*` padding, deliberately-invalid witnesses) +// are handled in `test_runner.rs`. #[cfg(feature = "stateless")] const EXTRA_SKIPS: &[&str] = &[]; #[cfg(not(any(feature = "sp1", feature = "stateless")))] diff --git a/tooling/ef_tests/engine/Makefile b/tooling/ef_tests/engine/Makefile index 551a35baa6b..ca58a742518 100644 --- a/tooling/ef_tests/engine/Makefile +++ b/tooling/ef_tests/engine/Makefile @@ -10,6 +10,13 @@ AMSTERDAM_FIXTURES_FILE := ../.fixtures_url_amsterdam AMSTERDAM_ARTIFACT := amsterdam-tests.tar.gz AMSTERDAM_URL := $(shell cat $(AMSTERDAM_FIXTURES_FILE)) +# Subtrees the Amsterdam bundle contributes. `for_bpo2toamsterdamattime15k` holds the +# fork-activation fixtures (a header field appearing mid-chain, the gas schedule +# switching between blocks); it exists only in this bundle, so omitting it leaves the +# whole fork boundary untested. +AMSTERDAM_SUBTREES := for_amsterdam for_bpo2toamsterdamattime15k +AMSTERDAM_STAMP := $(SPECTEST_VECTORS_DIR)/.amsterdam_overlay + VECTORS_TARGETS := $(SPECTEST_VECTORS_DIR) $(SPECTEST_ARTIFACT): $(FIXTURES_FILE) @@ -21,13 +28,22 @@ $(SPECTEST_VECTORS_DIR): $(SPECTEST_ARTIFACT) mkdir -p $(SPECTEST_VECTORS_DIR) tar -xzf $(SPECTEST_ARTIFACT) --strip-components=2 -C $(SPECTEST_VECTORS_DIR) fixtures/blockchain_tests_engine -$(AMSTERDAM_ARTIFACT): $(AMSTERDAM_FIXTURES_FILE) +# Order-only on $(SPECTEST_VECTORS_DIR): building it runs clean-vectors, which deletes +# $(AMSTERDAM_ARTIFACT). Downloading afterwards keeps the bundle from being wiped +# between the curl and the extract. +$(AMSTERDAM_ARTIFACT): $(AMSTERDAM_FIXTURES_FILE) | $(SPECTEST_VECTORS_DIR) curl -L -o $(AMSTERDAM_ARTIFACT) $(AMSTERDAM_URL) -$(SPECTEST_VECTORS_DIR)/for_amsterdam: $(AMSTERDAM_ARTIFACT) $(SPECTEST_VECTORS_DIR) - tar -xzf $(AMSTERDAM_ARTIFACT) --strip-components=2 -C $(SPECTEST_VECTORS_DIR) fixtures/blockchain_tests_engine/for_amsterdam +# Clear the subtrees first: the bundle is overlaid onto an existing `vectors/eest`, so +# fixtures renamed or dropped between devnet releases would otherwise survive as stale +# leftovers and run against the current client. +$(AMSTERDAM_STAMP): $(AMSTERDAM_ARTIFACT) $(SPECTEST_VECTORS_DIR) + rm -rf $(addprefix $(SPECTEST_VECTORS_DIR)/,$(AMSTERDAM_SUBTREES)) + tar -xzf $(AMSTERDAM_ARTIFACT) --strip-components=2 -C $(SPECTEST_VECTORS_DIR) \ + $(addprefix fixtures/blockchain_tests_engine/,$(AMSTERDAM_SUBTREES)) + touch $@ -amsterdam-vectors: $(SPECTEST_VECTORS_DIR)/for_amsterdam ## 📥 Overlay glamsterdam-devnet-6 engine fixtures onto vectors/eest +amsterdam-vectors: $(AMSTERDAM_STAMP) ## 📥 Overlay Amsterdam engine fixtures onto vectors/eest help: ## 📚 Show help for each Makefile target @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/tooling/ef_tests/engine/src/exception_mapper.rs b/tooling/ef_tests/engine/src/exception_mapper.rs index 78b74ae8880..e52bc54df8d 100644 --- a/tooling/ef_tests/engine/src/exception_mapper.rs +++ b/tooling/ef_tests/engine/src/exception_mapper.rs @@ -1,7 +1,8 @@ //! Maps EEST canonical exception names (e.g. `TransactionException.NONCE_MISMATCH_TOO_LOW`) //! to ethrex's actual error wording. //! -//! Ported from `execution-specs/packages/testing/src/execution_testing/client_clis/clis/ethrex.py`. +//! Ported from `execution-specs/packages/testing/src/execution_testing/client_clis/clis/ethrex.py`, +//! verified against `tests-glamsterdam-devnet@v8.0.0`. //! When the Python mapper is updated (new exceptions or reworded ethrex messages), this file //! must be updated in lock-step. The tests-vs-mapper drift is the main source of //! `validation_error` mismatch noise; keep this honest. diff --git a/tooling/ef_tests/engine/src/fixture.rs b/tooling/ef_tests/engine/src/fixture.rs index 63a70dd1347..d3bdfcf0608 100644 --- a/tooling/ef_tests/engine/src/fixture.rs +++ b/tooling/ef_tests/engine/src/fixture.rs @@ -52,6 +52,10 @@ pub struct FixtureHeader { pub parent_beacon_block_root: Option, #[serde(default)] pub requests_hash: Option, + /// EIP-7843 genesis slot number. Non-zero in fixtures that start the chain at a + /// slot other than 0, and part of the genesis header hash. + #[serde(default)] + pub slot_number: Option, } #[derive(Debug, Deserialize)] @@ -402,6 +406,7 @@ fn build_genesis_json(header: &FixtureHeader, alloc: &Value, config: Value) -> V insert_opt!("excessBlobGas", header.excess_blob_gas); insert_opt!("parentBeaconBlockRoot", header.parent_beacon_block_root); insert_opt!("requestsHash", header.requests_hash); + insert_opt!("slotNumber", header.slot_number); genesis } diff --git a/tooling/ef_tests/state/Makefile b/tooling/ef_tests/state/Makefile index d72a6a85ee2..f60735e89ae 100644 --- a/tooling/ef_tests/state/Makefile +++ b/tooling/ef_tests/state/Makefile @@ -25,13 +25,19 @@ $(VECTORS_DIR): $(STATETEST_ARTIFACT) $(MAKE) setup-test-dirs $(MAKE) clone-ef-tests tar -xzf $(STATETEST_ARTIFACT) --strip-components=2 -C $(VECTORS_DIR)/state_tests fixtures/state_tests - rm -f $(STATETEST_ARTIFACT) rm -rf $(TMP_DIR) -$(AMSTERDAM_ARTIFACT): $(AMSTERDAM_FIXTURES_FILE) +# Order-only on $(VECTORS_DIR): building it runs clean-evm-ef-tests, which deletes +# $(AMSTERDAM_ARTIFACT). Downloading afterwards keeps the bundle from being wiped +# between the curl and the extract. +$(AMSTERDAM_ARTIFACT): $(AMSTERDAM_FIXTURES_FILE) | $(VECTORS_DIR) curl -L -o $(AMSTERDAM_ARTIFACT) $(AMSTERDAM_URL) +# Clear the subtree first: the bundle is overlaid onto an existing tree, so fixtures +# renamed or dropped between devnet releases would otherwise survive as stale +# leftovers and run against the current client. $(VECTORS_DIR)/state_tests/for_amsterdam: $(AMSTERDAM_ARTIFACT) $(VECTORS_DIR) + rm -rf $(VECTORS_DIR)/state_tests/for_amsterdam tar -xzf $(AMSTERDAM_ARTIFACT) --strip-components=2 -C $(VECTORS_DIR)/state_tests fixtures/state_tests/for_amsterdam amsterdam-vectors: $(VECTORS_DIR)/state_tests/for_amsterdam @@ -70,10 +76,10 @@ run-evm-ef-tests: ## 🏃‍♂️ Run EF Tests time cargo test --test all --profile release-fast -- $(flags);\ fi -run-evm-ef-tests-ci: $(VECTORS_DIR) ## 🏃‍♂️ Run EF Tests only with LEVM and without spinner, for CI. +run-evm-ef-tests-ci: $(VECTORS_DIR) amsterdam-vectors ## 🏃‍♂️ Run EF Tests only with LEVM and without spinner, for CI. time cargo test -p ef_tests-state --test all --profile release-fast -- --summary -test-levm: $(VECTORS_DIR) +test-levm: $(VECTORS_DIR) amsterdam-vectors $(MAKE) run-evm-ef-tests flags="--summary" test-levm-nostd-crypto: $(VECTORS_DIR) ## 🧪 Run EF state tests with no_std crypto fallbacks