EIP-7928 Block-level access lists
No call decision · Scaling & data · EIP status Last Call · created 2025-03-31
Makes a block access list (BAL) mandatory, containing every account and storage slot the block accessed along with post-execution values, and puts its hash in the block header so that disk reads and transaction validation can be processed in parallel.
Why it is needed
Today, when an Ethereum node receives a block, it executes the transactions one by one in order. This is because it cannot know which accounts and storage a transaction will touch until it runs it. The EIP-2930 access list lets each transaction declare the addresses it will access in advance, but it is optional and not enforced. This proposal makes the access list mandatory at the block level. If every address and storage location the block accessed, along with post-execution values, arrives as a list, a node can read the needed data in advance in parallel, execute non-overlapping transactions concurrently, and parallelize the post-execution state root computation. It also becomes possible to update state from the list alone, without executing the transactions directly. The original text explains that execution time drops to roughly 'parallel IO + parallel EVM'.
What changes
Today
- Transactions are executed in order because it is impossible to know before execution which state a transaction will touch
- The EIP-2930 access list is optional, so it cannot be relied on as a basis for parallelization
- Restoring or syncing state requires re-executing the transactions directly
After EIP-7928
- Each block comes with a list of the addresses, slots and post-execution values it accessed, verified by a hash in the header
- Needed data can be read in advance and non-overlapping transactions can be executed concurrently
- State can be updated using only the list's state diffs, without executing the transactions
Who is affected
- Client teams: Implementers must add a block header field, BAL generation and validation, and new Engine API methods (engine_newPayloadV5, engine_getPayloadV6, and others). This change requires a hard fork.
- Validators and node operators: Block processing can use parallel IO and parallel execution, at the cost of propagating and storing a BAL averaging about 92.1KiB (compressed).
- Block builders and proposers: When building a block they must also produce an exact BAL, and if any entry is missing or fabricated, the entire block becomes invalid.
- Ordinary users: They have nothing to do directly, but benefit indirectly from faster block validation on nodes.
In depth
Only the hash in the header, the BAL outside the block
A block_access_list_hash field is added to the block header. Its value is the Keccak-256 hash of the RLP-encoded BAL, and for an empty list it is keccak256(rlp.encode([])) = 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347. The BAL itself is not included in the block body. The execution layer stores it separately and passes it as an ExecutionPayload field in the Engine API, and the consensus layer keeps it as a field of the SSZ ExecutionPayload container. For long-term storage, the full bytes can be deleted and only the SSZ hash_tree_root of that field kept, preserving the Merkle commitment.
RLP structure, block access index, ordering rules
A BAL is an RLP list that follows the order address → field → block access index → change. For each account it records AccountChanges = [address, storage_changes, storage_reads, balance_changes, nonce_changes, code_changes]. BlockAccessIndex is 0 for pre-execution system calls, 1..n for transactions in block order, and n+1 for post-execution, including withdrawals. Integer fields (StorageKey, StorageValue, Balance, Nonce, BlockAccessIndex) must be encoded as minimal big-endian with no leading zeros, and 0 is encoded as an empty byte string (0x80). Because the hash commits to these bytes, using a different encoding for the same value, such as 32-byte padding, makes the block invalid. Ordering is also enforced. Accounts are sorted lexicographically by address, slots and read keys by key ascending, and each change list by index ascending. Each address must appear exactly once in the list, and the same slot cannot appear in both storage_changes and storage_reads.
What goes into the list
Addresses that were only accessed, not just those whose state changed, are included with an empty change list. The targets of BALANCE, EXTCODESIZE, EXTCODECOPY and EXTCODEHASH, the targets of CALL, CALLCODE, DELEGATECALL and STATICCALL (included even if reverted), the transaction sender and recipient (including zero-value transfers), the SELFDESTRUCT beneficiary, precompiles, withdrawal recipients, and accessed system contracts are all covered. The target address of CREATE/CREATE2 is included only when the EVM actually accesses the computed address, not from address computation alone, and the deployment address of a deployment transaction (one whose to is empty) is always included. The system caller address SYSTEM_ADDRESS (0xfffffffffffffffffffffffffffffffffffffffe) is not added unless it has its own state access. Entries in an EIP-2930 transaction access list are not included automatically; only addresses that were actually accessed are recorded.
Size limit: tied to the gas limit
The list size is bound by the block gas limit instead of a fixed cap. bal_items <= block_gas_limit // ITEM_COST, where ITEM_COST = 2000. bal_items is the sum of the total number of storage keys and the number of unique addresses. With EIP-7981 applied, the cheapest way to add one item is a cold SLOAD (COLD_SLOAD_COST = 2100), so ITEM_COST is deliberately set below that, which creates headroom of about block_gas_limit / 42000 items. This headroom absorbs the entries for system contract executions that do not consume block gas (EIP-2935, EIP-4788, EIP-7002, EIP-7251) and for withdrawal recipients (EIP-4895).
Two-stage gas validation and the inclusion decision
Gas validation for instructions that access state is split into two stages. Costs that can be known without looking at state (base cost, memory expansion, warm/cold access cost) are validated up front, while costs that need state, such as whether an account exists or resolving an EIP-7702 delegation, are validated afterwards. If the up-front validation fails, the target was not accessed and must not be put in the BAL. If it passes, the target stays in the BAL even when a later cost check fails. SSTORE implicitly reads the current value to compute gas, and before that the slot's access_cost and the GAS_CALL_STIPEND sentry (gas_left > GAS_CALL_STIPEND) must both pass. If either fails, the slot is put in neither storage_reads nor storage_changes. Calling a target that carries an EIP-7702 delegation includes both the original target and the delegated address, but if the gas check for the delegated address's own access_cost fails, the delegated address is not included.
Recording rules by change type
Storage goes into storage_changes only for writes that actually change the value (including setting it to 0). Slots that were only read, and no-op writes that store the same value again, go into storage_reads. Whether a write counts is decided by comparing against the accumulated state just before the current block access index. For balances, the final post-transaction balance (uint256) is recorded, but if the balance changed within the transaction and ended up equal to its pre-transaction value, it is not put in balance_changes and only the address is included. A zero-value block reward does not by itself create a balance change or an address inclusion. For nonces, the post-transaction values are recorded for EOA senders, contracts that succeeded in CREATE/CREATE2, deployed contracts, and EIP-7702 authorities. For code, the post-transaction runtime bytecode of deployed or modified contracts and successful delegation indicators are recorded. If even one entry is missing or fabricated, the block is invalid.
Engine API extensions and retention period
ExecutionPayloadV4, which extends ExecutionPayloadV3, adds a blockAccessList field. engine_newPayloadV5 returns INVALID if the received BAL is malformed or differs from the execution result, and engine_getPayloadV6 builds the payload with blockAccessList filled from the accesses collected during execution. Historical BALs are queried with engine_getPayloadBodiesByHashV2 and engine_getPayloadBodiesByRangeV2, and the result is null for blocks before Amsterdam or when the data has been pruned. The execution layer must serve BALs through these methods for at least HISTORY_PRUNE_EPOCHS (14299 epochs), the block body retention period.
Size and security analysis, current status and call decisions
Based on an analysis of 1000 mainnet blocks in October 2026, the average BAL size is about 92.1 KiB after compression (60m gas limit). 60 to 80% of transactions access non-overlapping slots and can be parallelized directly, and the remaining 20 to 40% can also be parallelized using post-transaction state diffs. On the security side, storage_reads is not tied to a specific transaction index, so verifying it requires executing every transaction, which lets a malicious proposer declare fake reads of slots that will never be accessed and cause unnecessary I/O. The bal_items limit bounds that count, and clients may add detection safeguards during execution but must not reject valid blocks. The status of the source is Last Call (deadline 2026-11-01). In the call record, on 07.09 ACDC #182 it was confirmed that the EIP-7928 change would be included in Devnet 7 rather than deferred to Devnet 8. On 09.21 ACDT #97 the Glamsterdam CL block serving window and the BAL retention period were left undecided and passed to async discussion and ACDE (three options were presented: keep 33,024 epochs, reduce to about 14,299, reduce to 8,192, and none of them blocks the testnet launch).