EIP-7928 defines block-level access lists recording all accessed accounts and storage slots with post-execution values.
It enables parallel disk reads, executionless state updates, and faster validation.
This EIP introduces Block-Level Access Lists (BALs) that record all accounts and storage locations accessed during block execution, along with their post-execution values. BALs enable parallel disk reads, parallel transaction validation, parallel state root computation and executionless state updates.
Transaction execution cannot be parallelized without knowing in advance which addresses and storage slots will be accessed. While EIP-2930 introduced optional transaction access lists, they are not enforced.
This proposal enforces access lists at the block level, enabling:
parallel IO + parallel EVMWe introduce a new field to the block header, block_access_list_hash, which contains the Keccak-256 hash of the RLP-encoded block access list. When no state changes are present, this field is the hash of an empty RLP list 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, i.e. keccak256(rlp.encode([])).
The BlockAccessList is not included in the block body. The EL stores BALs separately and transmits them as a field in the ExecutionPayload via the engine API. The BAL is RLP-encoded as a list of AccountChanges. When no state changes are present, this field is the empty RLP list 0xc0, i.e. rlp.encode([]).
BALs use RLP encoding following the pattern: address -> field -> block_access_index -> change.
BlockAccessList is the set of all addresses accessed during block execution.
It MUST include:
Addresses with state changes (storage, balance, nonce, or code).
Addresses accessed without state changes, including:
BALANCE, EXTCODESIZE, EXTCODECOPY, EXTCODEHASH opcodesCALL, CALLCODE, DELEGATECALL, STATICCALL (even if they revert; see Gas Validation Before State Access for inclusion conditions)CREATE/CREATE2 if the target account is accessed0x0 with initcode)SELFDESTRUCTSYSTEM_ADDRESS (0xfffffffffffffffffffffffffffffffffffffffe), MUST NOT be included unless it experiences state access itselfAddresses with no state changes MUST still be present with empty change lists.
Entries from an EIP-2930 access list MUST NOT be included automatically. Only addresses and storage slots that are actually touched or changed during execution are recorded.
The block access list is constrained by the block gas limit rather than a fixed maximum number of items. The constraint is defined as:
Where:
bal_items = storage_keys + addressesITEM_COST = 2000The storage_keys is the total number of storage keys across all accounts, and addresses is the total number of unique addresses accessed in the block. With EIP-7981, the cheapest way to add an item to the BAL is a cold SLOAD at COLD_SLOAD_COST (2100, as defined in EIP-2929). ITEM_COST is set deliberately below this minimum to create a buffer of approximately block_gas_limit / 42000 extra items, which absorbs BAL entries from system contract execution (e.g., EIP-2935, EIP-4788, EIP-7002, EIP-7251) and withdrawal recipients (EIP-4895) that do not consume block gas.
State-accessing opcodes perform gas validation in two phases:
Pre-state validation MUST pass before any state access occurs. If pre-state validation fails, the target resource (address or storage slot) is never accessed and MUST NOT be included in the BAL.
Once pre-state validation passes, the target is accessed and included in the BAL. Post-state costs are then calculated; their order is implementation-defined since the target has already been accessed.
The following table specifies pre-state validation costs in addition to the base opcode cost (gas constants as defined in EIP-2929):
| Instruction | Pre-state Validation |
|---|---|
BALANCE | access_cost |
SELFBALANCE | None (accesses current contract, always warm) |
EXTCODESIZE | access_cost |
EXTCODEHASH | access_cost |
EXTCODECOPY | access_cost + memory_expansion |
CALL | access_cost + memory_expansion + GAS_CALL_VALUE (if value > 0) |
CALLCODE | access_cost + memory_expansion + GAS_CALL_VALUE (if value > 0) |
DELEGATECALL | access_cost + memory_expansion |
STATICCALL | access_cost + memory_expansion |
CREATE | memory_expansion + INITCODE_WORD_COST + GAS_CREATE |
CREATE2 | memory_expansion + INITCODE_WORD_COST + GAS_KECCAK256_WORD + GAS_CREATE |
SLOAD | access_cost |
SSTORE | More than GAS_CALL_STIPEND available |
SELFDESTRUCT | GAS_SELF_DESTRUCT + access_cost |
Where:
access_cost: For account-accessing opcodes: COLD_ACCOUNT_ACCESS_COST if cold, WARM_STORAGE_READ_COST if warm. For storage-accessing opcodes (SLOAD): COLD_SLOAD_COST if cold, WARM_STORAGE_READ_COST if warm.memory_expansion: Gas cost to expand memory for input/output regionsPost-state costs (e.g., GAS_NEW_ACCOUNT for calls to empty accounts, GAS_SELF_DESTRUCT_NEW_ACCOUNT if beneficiary does not exist) do not affect BAL inclusion since the target has already been accessed.
When a call target has an EIP-7702 delegation, the target is accessed to resolve the delegation. If a delegation exists, the delegated address requires its own access_cost check before being accessed. If this check fails, the delegated address MUST NOT appear in the BAL, though the original call target is included (having been accessed to resolve the delegation).
Note: Delegated accounts cannot be empty, so GAS_NEW_ACCOUNT never applies when resolving delegations.
SSTORE performs an implicit read of the current storage value for gas calculation. The GAS_CALL_STIPEND check prevents this state access when operating within the call stipend. If SSTORE fails this check, the storage slot MUST NOT appear in storage_reads or storage_changes.
The following ordering rules MUST apply:
The following uniqueness constraints MUST hold:
BlockAccessList.storage_changes per account.storage_reads per account.storage_changes and storage_reads for the same account.block_access_index MUST appear at most once per change list (balance_changes, nonce_changes, code_changes, and per-slot StorageChange list).BlockAccessIndex values MUST be assigned as follows:
0 for pre‑execution system contract calls.1 … n for transactions (in block order).n + 1 for post‑execution system contract calls.Writes include:
Reads include:
SLOAD that are not written.SSTORE where post-value equals pre-value, also known as "no-op writes").Note: Implementations MUST check the pre-transaction value to correctly distinguish between actual writes and no-op writes.
balance_changes)Record post‑transaction balances (uint256) for:
value > 0).value > 0).value > 0).For unaltered account balances:
If an account’s balance changes during a transaction, but its post-transaction balance is equal to its pre-transaction balance, then the change MUST NOT be recorded in balance_changes. The sender and recipient address MUST be included in AccountChanges.
The following special cases require addresses to be included with empty changes if no other state changes occur:
Zero-value block reward recipients MUST NOT trigger a balance change in the block access list and MUST NOT cause the recipient address to be included as a read (e.g. without changes). Zero-value block reward recipients MUST only be included with a balance change in blocks where the reward is greater than zero.
Track post‑transaction runtime bytecode for deployed or modified contracts, and delegation indicators for successful delegations as defined in EIP-7702.
Record post‑transaction nonces for:
CREATE or CREATE2.AccountChanges without nonce or code changes. However, if the account had a positive balance pre-transaction, the balance change to zero MUST be recorded. Storage keys within the self-destructed contracts that were modified or read MUST be included as a storage_reads entry.EXTCODEHASH, EXTCODESIZE, BALANCE, STATICCALL, etc.).balance_changes.storage_reads.block_access_index = 0.block_access_index = len(transactions) + 1.accessed_addresses (per EIP-2929), it MUST still be included with an empty change set; if authorization fails before the authority is loaded, it MUST NOT be included. The delegation target MUST NOT be included during delegation creation or modification and MUST only be included once it is actually loaded as an execution target (e.g., via CALL/CALLCODE/DELEGATECALL/STATICCALL under authority execution).MAX_WITHDRAWAL_REQUESTS_PER_BLOCK queue data slots (from slot 4 onward), which appear as storage_reads.MAX_CONSOLIDATION_REQUESTS_PER_BLOCK queue data slots (from slot 4 onward), which appear as storage_reads.The Engine API is extended with new structures and methods to support block-level access lists:
ExecutionPayloadV4 extends ExecutionPayloadV3 with:
blockAccessList: RLP-encoded block access listengine_newPayloadV5 validates execution payloads:
ExecutionPayloadV4 structureblockAccessListINVALID if access list is malformed or doesn't matchengine_getPayloadV6 builds execution payloads:
ExecutionPayloadV4 structureblockAccessList field with RLP-encoded access listBlock processing flow:
When processing a block:
block_access_list_hash = keccak256(blockAccessList) and includes it in the block headerThe execution layer provides the RLP-encoded blockAccessList to the consensus layer via the Engine API. The consensus layer then computes the SSZ hash_tree_root for storage in the ExecutionPayload.
Retrieval methods for historical BALs:
engine_getPayloadBodiesByHashV2: Returns ExecutionPayloadBodyV2 objects containing transactions, withdrawals, and blockAccessListengine_getPayloadBodiesByRangeV2: Returns ExecutionPayloadBodyV2 objects containing transactions, withdrawals, and blockAccessListThe blockAccessList field contains the RLP-encoded BAL or null for pre-Amsterdam blocks or when data has been pruned.
The EL MUST retain BALs for at least the duration of the weak subjectivity period (=3533 epochs) to support synchronization with re-execution after being offline for less than the WSP.
The state transition function must validate that the provided BAL matches the actual state accesses.
Implementation Note: The BAL itself does not need to enter the state transition function. Implementations MAY validate by generating a virtual BAL during execution, hashing it, and comparing against the block_access_list_hash in the header. This is the approach used in the execution-specs reference implementation.
The BAL MUST be complete and accurate. Missing or spurious entries invalidate the block. Spurious entries MAY be detected by validating BAL indices, which MUST never be higher than len(transactions) + 1.
Clients MAY invalidate immediately if any transaction exceeds declared state.
Clients MUST store BALs separately from blocks and make them available via the engine API.
Example block:
Pre-execution:
0x0000F90827F1C53a10cb7A02335B175320002935)Transactions:
Post-execution:
Note: Pre-execution system contract uses block_access_index = 0. Post-execution withdrawal uses block_access_index = 3 (len(transactions) + 1)
Resulting BAL (RLP structure):
RLP-encoded and compressed: ~400-500 bytes.
This design variant was chosen for several key reasons:
Size vs parallelization: BALs include all accessed addresses (even unchanged) for complete parallel IO and execution.
Storage values for writes: Post-execution values enable state reconstruction during sync without individual proofs against state root.
Overhead analysis: Historical data shows ~70 KiB average BAL size.
Transaction independence: 60-80% of transactions access disjoint storage slots, enabling effective parallelization. The remaining 20-40% can be parallelized by having post-transaction state diffs.
RLP encoding: Native Ethereum encoding format, maintains compatibility with existing infrastructure.
Average BAL size: ~72.4 KiB (compressed)
Smaller than current worst-case calldata blocks.
An empirical analysis has been done here. An updated analysis for a 60 million block gas limit can be found here.
BAL verification occurs alongside parallel IO and EVM operations without delaying block processing.
This proposal requires changes to the block structure and engine API that are not backwards compatible and require a hard fork.
Validating access lists and balance diffs adds validation overhead but is essential to prevent acceptance of invalid blocks.
Increased block size impacts propagation but overhead (~70 KiB average) is reasonable for performance gains.
Since storage_reads entries are not mapped to specific transaction indices, their validity can only be confirmed after executing all transactions. A malicious proposer could exploit this by declaring phantom storage reads that are never accessed, forcing clients into unnecessary I/O prefetching and significant data download while the block remains unrejectable until completion.
To mitigate this, clients SHOULD enforce a gas-budget feasibility check at transaction boundaries. Let:
R_remaining = number of declared storage reads not yet accessedG_remaining = remaining block gasThe following invariant must hold:
Where 2000 is the minimum gas cost for a storage read (via EIP-2930 access lists: 1900 upfront + 100 warm read). If this check fails, the block can be rejected immediately as invalid, since insufficient gas remains to access the declared reads. This check SHOULD be performed periodically (e.g., every 8 transactions) to enable early rejection without impacting parallel execution.
Copyright and related rights waived via CC0.