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.
Motivation
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 disk reads and transaction execution
Parallel post-state root calculation
State reconstruction without executing transactions
Reduced execution time to parallel IO + parallel EVM
Specification
Block Structure Modification
We 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([]).
RLP Data Structures
BALs use RLP encoding following the pattern: address -> field -> block_access_index -> change.
# Type aliases for RLP encoding
Address=bytes20# 20-byte Ethereum address
StorageKey=uint256# Storage slot key
StorageValue=uint256# Storage value
Bytecode=bytes# Variable-length contract bytecode
BlockAccessIndex=uint16# Block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution)
Balance=uint256# Post-transaction balance in wei
Nonce=uint64# Account nonce
# Core change structures (RLP encoded as lists)
# StorageChange: [block_access_index, new_value]
StorageChange=[BlockAccessIndex,StorageValue]# BalanceChange: [block_access_index, post_balance]
BalanceChange=[BlockAccessIndex,Balance]# NonceChange: [block_access_index, new_nonce]
NonceChange=[BlockAccessIndex,Nonce]# CodeChange: [block_access_index, new_code]
CodeChange=[BlockAccessIndex,Bytecode]# SlotChanges: [slot, [changes]]
# All changes to a single storage slot
SlotChanges=[StorageKey,List[StorageChange]]# AccountChanges: [address, storage_changes, storage_reads, balance_changes, nonce_changes, code_changes]
# All changes for a single account, grouped by field type
AccountChanges=[Address,# address
List[SlotChanges],# storage_changes (slot -> [block_access_index -> new_value])
List[StorageKey],# storage_reads (read-only storage keys)
List[BalanceChange],# balance_changes ([block_access_index -> post_balance])
List[NonceChange],# nonce_changes ([block_access_index -> new_nonce])
List[CodeChange]# code_changes ([block_access_index -> new_code])
]# BlockAccessList: List of AccountChanges
BlockAccessList=List[AccountChanges]
Scope and Inclusion
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:
Targets of BALANCE, EXTCODESIZE, EXTCODECOPY, EXTCODEHASH opcodes
Targets of CALL, CALLCODE, DELEGATECALL, STATICCALL (even if they revert)
Note: For call operations (CALL, CALLCODE, DELEGATECALL, STATICCALL), the target address is only included in the BAL if gas checks (memory expansion, access cost, transfer cost) pass; if gas shortage causes the operation to fail before state access, the target address MUST NOT be included.
Target addresses of CREATE/CREATE2 (even when creation fails)
Deployed contract addresses from calls with initcode to empty addresses (e.g., calling 0x0 with initcode)
Transaction sender and recipient addresses (even for zero-value transfers)
COINBASE address when receiving transaction fees
Beneficiary addresses for SELFDESTRUCT
System contract addresses accessed during pre/post-execution; the system caller address, SYSTEM_ADDRESS (0xfffffffffffffffffffffffffffffffffffffffe), MUST NOT be included unless it experiences state access itself
Withdrawal recipient addresses
Precompiled contracts when called or accessed
Addresses 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.
Ordering and Determinism
The following ordering rules MUST apply:
Accounts: Lexicographic by address
storage_changes: Slots lexicographic by storage key; within each slot, changes by block access index index (ascending)
storage_reads: Lexicographic by storage key
balance_changes, nonce_changes, code_changes: By block access index (ascending)
BlockAccessIndex Assignment
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.
Recording Semantics by Change Type
Storage
Writes include:
Any value change (post‑value ≠ pre‑value).
Zeroing a slot (pre‑value exists, post‑value is zero).
Reads include:
Slots accessed via SLOAD that are not written.
Slots written with unchanged values (i.e., 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.
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.
This includes the following special cases where addresses MUST be included with empty changes if no other state changes occur. This includes:
Zero-value transfer recipients
Calling a same-transaction SELFDESTRUCT on an address that had a zero pre-transaction balance
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.
Code
Track post‑transaction runtime bytecode for deployed or modified contracts, and delegation indicators for successful delegations as defined in EIP-7702.
Nonce
Record post‑transaction nonces for:
EOA senders.
Contracts that performed a successful CREATE or CREATE2.
COINBASE / Fee Recipient: The COINBASE address MUST be included if it experiences any state change. It MUST NOT be included for blocks with no transactions, provided there are no other state changes (e.g., from EIP-4895 withdrawals). If the COINBASE reward is zero, the COINBASE address MUST be included as a read.
Precompiled contracts: Precompiles MUST be included when accessed. If a precompile receives value, it is recorded with a balance change. Otherwise, it is included with empty change lists.
SENDALL: For positive-value selfdestructs, the sender and beneficiary are recorded with a balance change.
SELFDESTRUCT (in-transaction): Accounts destroyed within a transaction MUST be included in 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_read.
Accessed but unchanged: Include the address with empty changes (e.g., targets of EXTCODEHASH, EXTCODESIZE, BALANCE, STATICCALL, etc.).
Zero‑value transfers / Unchanged balance in transaction: Include the address; omit from balance_changes.
Gas refunds: Record the final balance of the sender after each transaction.
Block rewards: Record the final balance of the fee recipient after each transaction.
Exceptional halts: Record the final nonce and balance of the sender, and the final balance of the fee recipient after each transaction. State changes from the reverted call are discarded, but all accessed addresses MUST be included. If no changes remain, addresses are included with empty lists; if storage was read, the corresponding keys MUST appear in storage_reads.
Pre‑execution system contract calls: All state changes MUST use block_access_index = 0.
Post‑execution system contract calls: All state changes MUST use block_access_index = len(transactions) + 1.
EIP-7702 Delegations: The authority address MUST be included with the nonce and code changes after any successful delegation set, reset, or update. Additionally it MUST be included with an empty change set if authorization fails, but the authority address has already been added to accessed_addresses (as defined in EIP-2929). The delegation target MUST NOT be included during delegation creation and MUST be included when loaded as a call target under authority execution.
EIP‑4895 (Consensus layer withdrawals): Recipients are recorded with their final balance after the withdrawal.
EIP‑2935 (block hash): Record system contract storage diffs of the single updated storage slot in the ring buffer.
EIP‑4788 (beacon root): Record system contract storage diffs of the two updated storage slots in the ring buffer.
EIP‑7002 (withdrawals): Record system contract storage diffs of storage slots 0–3 (4 slots) after the dequeuing call.
EIP‑7251 (consolidations): Record system contract storage diffs of storage slots 0–3 (4 slots) after the dequeuing call.
Engine API
The Engine API is extended with new structures and methods to support block-level access lists:
Validates that computed access list matches provided blockAccessList
Returns INVALID if access list is malformed or doesn’t match
engine_getPayloadV6 builds execution payloads:
Returns ExecutionPayloadV4 structure
Collects all account accesses and state changes during transaction execution
Populates blockAccessList field with RLP-encoded access list
Block processing flow:
When processing a block:
The EL receives the BAL in the ExecutionPayload
The EL computes block_access_list_hash = keccak256(blockAccessList) and includes it in the block header
The EL executes the block and generates the actual BAL
If the generated BAL doesn’t match the provided BAL, the block is invalid (the hash in the header would be wrong)
The 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 (similar to engine_getPayloadBodiesByHashV1):
engine_getBALsByHashV1: Array of block hashes → Array of RLP-encoded BALs
engine_getBALsByRangeV1: Start block number, count → Array of RLP-encoded BALs
The EL MUST retain BALs for at least the duration the weak subjectivity period (=3533 epochs) to support synchronization with re-execution after being offline for less than the WSP.
State Transition Function
The state transition function must validate that the provided BAL matches the actual state accesses:
defvalidate_block(execution_payload,block_header):# 1. Compute hash from received BAL and set in header
block_header.block_access_list_hash=keccak(execution_payload.blockAccessList)# 2. Execute block and collect actual accesses
actual_bal=execute_and_collect_accesses(execution_payload)# 3. Verify actual execution matches provided BAL
# If this fails, the block is invalid (the hash in the header would be wrong)
assertrlp.encode(actual_bal)==execution_payload.blockAccessListdefexecute_and_collect_accesses(block):"""Execute block and collect all state accesses into BAL format"""accesses={}# Pre-execution system contracts (block_access_index = 0)
track_system_contracts_pre(block,accesses,block_access_index=0)# Execute transactions (block_access_index = 1..n)
fori,txinenumerate(block.transactions):execute_transaction(tx)track_state_changes(tx,accesses,block_access_index=i+1)# Withdrawals and post-execution (block_access_index = len(txs) + 1)
post_index=len(block.transactions)+1forwithdrawalinblock.withdrawals:apply_withdrawal(withdrawal)track_balance_change(withdrawal.address,accesses,post_index)track_system_contracts_post(block,accesses,post_index)# Convert to BAL format and sort
returnbuild_bal(accesses)deftrack_state_changes(tx,accesses,block_access_index):"""Track all state changes from a transaction"""foraddringet_touched_addresses(tx):ifaddrnotinaccesses:accesses[addr]={'storage_writes':{},# slot -> [(index, value)]
'storage_reads':set(),'balance_changes':[],'nonce_changes':[],'code_changes':[]}# Track storage changes
forslot,valueinget_storage_writes(addr).items():ifslotnotinaccesses[addr]['storage_writes']:accesses[addr]['storage_writes'][slot]=[]accesses[addr]['storage_writes'][slot].append((block_access_index,value))# Track reads (slots accessed but not written)
forslotinget_storage_reads(addr):ifslotnotinaccesses[addr]['storage_writes']:accesses[addr]['storage_reads'].add(slot)# Track balance, nonce, code changes
ifbalance_changed(addr):accesses[addr]['balance_changes'].append((block_access_index,get_balance(addr)))ifnonce_changed(addr):accesses[addr]['nonce_changes'].append((block_access_index,get_nonce(addr)))ifcode_changed(addr):accesses[addr]['code_changes'].append((block_access_index,get_code(addr)))defbuild_bal(accesses):"""Convert collected accesses to BAL format"""bal=[]foraddrinsorted(accesses.keys()):# Sort addresses lexicographically
data=accesses[addr]# Format storage changes: [slot, [[index, value], ...]]
storage_changes=[[slot,sorted(changes)]forslot,changesinsorted(data['storage_writes'].items())]# Account entry: [address, storage_changes, reads, balance_changes, nonce_changes, code_changes]
bal.append([addr,storage_changes,sorted(list(data['storage_reads'])),sorted(data['balance_changes']),sorted(data['nonce_changes']),sorted(data['code_changes'])])returnbal
The BAL MUST be complete and accurate. Missing or spurious entries invalidate the block.
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.
Concrete Example
Example block:
Pre-execution:
EIP-2935: Store parent hash at block hash contract (0x0000F90827F1C53a10cb7A02335B175320002935)
EIP-7002: Omitted for simplicity.
Transactions:
Alice (0xaaaa…) sends 1 ETH to Bob (0xbbbb…), checks balance of 0x2222…
Charlie (0xcccc…) calls factory (0xffff…) deploying contract at 0xdddd…
[# Addresses are sorted lexicographically
[# AccountChanges for 0x0000F90827F1C53a10cb7A02335B175320002935 (Block hash contract)
0x0000F90827F1C53a10cb7A02335B175320002935,[# storage_changes
[b'\x00...\x0f\xa0',[[0,b'...']]]# slot, [[block_access_index, parent_hash]]
],[],# storage_reads
[],# balance_changes
[],# nonce_changes
[]# code_changes
],[# AccountChanges for 0x2222... (Address checked by Alice)
0x2222...,[],# storage_changes
[],# storage_reads
[],# balance_changes (no change, just checked)
[],# nonce_changes
[]# code_changes
],[# AccountChanges for 0xaaaa... (Alice - sender tx 0)
0xaaaa...,[],# storage_changes
[],# storage_reads
[[1,0x...29a241a]],# balance_changes: [[block_access_index, post_balance]]
[[1,10]],# nonce_changes: [[block_access_index, new_nonce]]
[]# code_changes
],[# AccountChanges for 0xabcd... (Eve - withdrawal recipient)
0xabcd...,[],# storage_changes
[],# storage_reads
[[3,0x...5f5e100]],# balance_changes: 100 ETH withdrawal
[],# nonce_changes
[]# code_changes
],[# AccountChanges for 0xbbbb... (Bob - recipient tx 0)
0xbbbb...,[],# storage_changes
[],# storage_reads
[[1,0x...b9aca00]],# balance_changes: +1 ETH
[],# nonce_changes
[]# code_changes
],[# AccountChanges for 0xcccc... (Charlie - sender tx 1)
0xcccc...,[],# storage_changes
[],# storage_reads
[[2,0x...bc16d67]],# balance_changes: after gas
[[2,5]],# nonce_changes
[]# code_changes
],[# AccountChanges for 0xdddd... (Deployed contract)
0xdddd...,[],# storage_changes
[],# storage_reads
[],# balance_changes
[[2,1]],# nonce_changes: new contract nonce
[[2,b'\x60\x80\x60\x40...']]# code_changes: deployed bytecode
],[# AccountChanges for 0xeeee... (COINBASE)
0xeeee...,[],# storage_changes
[],# storage_reads
[[1,0x...05f5e1],[2,0x...0bebc2]],# balance_changes: after tx fees
[],# nonce_changes
[]# code_changes
],[# AccountChanges for 0xffff... (Factory contract)
0xffff...,[# storage_changes
[b'\x00...\x01',[[2,b'\x00...\xdd\xdd...']]]# slot 1, deployed address
],[],# storage_reads
[],# balance_changes
[[2,5]],# nonce_changes: after CREATE
[]# code_changes
]]
RLP-encoded and compressed: ~400-500 bytes.
Rationale
BAL Design Choice
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.
BAL Size Considerations (60m block gas limit)
Average BAL size: ~72.4 KiB (compressed)
Storage writes: ~29.2 KiB (40.3%)
Storage reads: ~18.7 KiB (25.8%)
Balance diffs: ~6.7 KiB (9.2%)
Nonce diffs: ~1.1 KiB (1.5%)
Code diffs: ~1.2 KiB (1.6%)
Account addresses (with diffs): ~7.7 KiB (10.7%)
Touched-only addresses: ~3.5 KiB (4.8%)
RLP encoding overhead: ~4.4 KiB (6.1%)
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.
Asynchronous Validation
BAL verification occurs alongside parallel IO and EVM operations without delaying block processing.
Backwards Compatibility
This proposal requires changes to the block structure and engine API that are not backwards compatible and require a hard fork.
Security Considerations
Validation Overhead
Validating access lists and balance diffs adds validation overhead but is essential to prevent acceptance of invalid blocks.
Block Size
Increased block size impacts propagation but overhead (~70 KiB average) is reasonable for performance gains.