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, 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
State reconstruction without executing transactions
Reduced execution time to parallel IO + parallel EVM
The block body includes a BlockAccessList containing all account accesses and state changes.
SSZ Data Structures
BALs use SSZ encoding following the pattern: address -> field -> tx_index -> change.
# Type aliases using SSZ types
Address=Bytes20# 20-byte Ethereum address
StorageKey=Bytes32# 32-byte storage slot key
StorageValue=Bytes32# 32-byte storage value
CodeData=List[byte,MAX_CODE_SIZE]# Variable-length contract bytecode
TxIndex=uint16# Transaction index within block (max 65,535)
Balance=uint128# Post-transaction balance in wei (16 bytes, sufficient for total ETH supply)
Nonce=uint64# Account nonce
# Constants; chosen to support a 630m block gas limit
MAX_TXS=30_000MAX_SLOTS=300_000MAX_ACCOUNTS=300_000MAX_CODE_SIZE=24_576# Maximum contract bytecode size in bytes
MAX_CODE_CHANGES=1# Core change structures (no redundant keys)
classStorageChange(Container):"""Single storage write: tx_index -> new_value"""tx_index:TxIndexnew_value:StorageValueclassBalanceChange(Container):"""Single balance change: tx_index -> post_balance"""tx_index:TxIndexpost_balance:BalanceclassNonceChange(Container):"""Single nonce change: tx_index -> new_nonce"""tx_index:TxIndexnew_nonce:NonceclassCodeChange(Container):"""Single code change: tx_index -> new_code"""tx_index:TxIndexnew_code:CodeData# Slot-level mapping (eliminates slot key redundancy)
classSlotChanges(Container):"""All changes to a single storage slot"""slot:StorageKeychanges:List[StorageChange,MAX_TXS]# Only changes, no redundant slot
# Account-level mapping (groups all account changes)
classAccountChanges(Container):"""
All changes for a single account, grouped by field type.
This eliminates address redundancy across different change types.
"""address:Address# Storage changes (slot -> [tx_index -> new_value])
storage_changes:List[SlotChanges,MAX_SLOTS]# Read-only storage keys
storage_reads:List[StorageKey,MAX_SLOTS]# Balance changes ([tx_index -> post_balance])
balance_changes:List[BalanceChange,MAX_TXS]# Nonce changes ([tx_index -> new_nonce])
nonce_changes:List[NonceChange,MAX_TXS]# Code changes ([tx_index -> new_code])
code_changes:List[CodeChange,MAX_CODE_CHANGES]# Block-level structure
classBlockAccessList(Container):"""
Block Access List
"""account_changes:List[AccountChanges,MAX_ACCOUNTS]
The BlockAccessList contains all addresses accessed during block execution:
Addresses with state changes (storage, balance, nonce, or code)
Addresses accessed without state changes (e.g., STATICCALL targets, BALANCE opcode targets)
Addresses with no state changes MUST still be included with empty change lists.
Ordering requirements:
Addresses: lexicographic (bytewise)
Storage keys: lexicographic within each account
Transaction indices: ascending within each change list
Storage writes include:
Value changes (different post-value than pre-value)
Zeroed slots (pre-value exists, post-value is zero)
Storage reads:
Slots accessed via SLOAD but not written
Slots written with unchanged values (SSTORE with same value)
Entries in an EIP-2930 access list MUST not automatically be included in the block_access_list - only addresses and storage slots that are actually touched or changed during execution are recorded.
Balance changes record post-transaction balances (uint128) for:
Zero-value transfers: MUST NOT be recorded in balance_changes but addresses MUST be included with empty AccountChanges.
Code changes track post-transaction runtime bytecode for deployed/modified contracts and delegation indicators from EIP-7702.
Nonce changes record post-transaction nonces for EOA senders, contracts that performed a successful CREATE or CREATE2 operation, deployed contracts and EIP-7702 authorities.
Important Implementation Details
Edge Cases
SELFDESTRUCT/SENDALL: Beneficiary recorded as balance change
Accessed but unchanged: Include with empty changes (EXTCODEHASH, EXTCODESIZE, BALANCE, STATICCALL, etc. targets)
Zero-value transfers: Include address, omit from balance_changes
Gas refunds: Final balance of sender recorded after each transactions
Block rewards: Final balance of fee recipient recorded after each transactions
Exceptional halts: Final nonce and balance of sender and balance of fee recipient recorded after each transactions
Consensus layer withdrawals (EIP-4895): Recipients recorded with final balance after withdrawal
Pre-execution system contract calls: All state changes MUST use tx_index = len(transactions)
Post-execution system contract calls: All state changes MUST use tx_index = len(transactions) +1
EIP-2935 block hash: System contract storage diffs of the single updated storage slot in the ring buffer
EIP-4788 beacon root: System contract storage diffs of the two updated storage slots the ring buffer
EIP-7002 withdrawals: System contract storage diffs of the storage slots 0-3 (4 slots) after dequeuing call
EIP-7251 consolidations: System contract storage diffs of the storage slots 0-3 (4 slots) after dequeuing call
Concrete Example
Example block:
Pre-execution:
EIP-2935: Store parent hash at block hash contract (0x0000…0001)
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…
BlockAccessList(account_changes=[# Addresses are sorted lexicographically
AccountChanges(address=0x0000000000000000000000000000000000000001,# Block hash contract (EIP-2935)
storage_changes=[SlotChanges(slot=b'\x00...\x0f\xa0',# slot = (block.number - 1) % 8191
changes=[StorageChange(tx_index=2,new_value=b'...')# Parent hash, pre-execution
])],storage_reads=[],balance_changes=[],nonce_changes=[],code_changes=[]),AccountChanges(address=0x2222...,# Address checked by Alice via BALANCE opcode
storage_changes=[],storage_reads=[],balance_changes=[],# No balance change, just checked
nonce_changes=[],code_changes=[]),AccountChanges(address=0xaaaa...,# Alice (sender tx 0)
storage_changes=[],storage_reads=[],balance_changes=[BalanceChange(tx_index=0,post_balance=0x...29a241a)],# After gas + 1 ETH sent
nonce_changes=[NonceChange(tx_index=0,new_nonce=10)],code_changes=[]),AccountChanges(address=0xabcd...,# Eve (withdrawal recipient)
storage_changes=[],storage_reads=[],balance_changes=[BalanceChange(tx_index=3,post_balance=0x...5f5e100)],# 100 ETH withdrawal
nonce_changes=[],code_changes=[]),AccountChanges(address=0xbbbb...,# Bob (recipient tx 0)
storage_changes=[],storage_reads=[],balance_changes=[BalanceChange(tx_index=0,post_balance=0x...b9aca00)],# +1 ETH
nonce_changes=[],code_changes=[]),AccountChanges(address=0xcccc...,# Charlie (sender tx 1)
storage_changes=[],storage_reads=[],balance_changes=[BalanceChange(tx_index=1,post_balance=0x...bc16d67)],# After gas
nonce_changes=[NonceChange(tx_index=1,new_nonce=5)],code_changes=[]),AccountChanges(address=0xdddd...,# Deployed contract
storage_changes=[],storage_reads=[],balance_changes=[],nonce_changes=[NonceChange(tx_index=1,new_nonce=1)],# New contract nonce
code_changes=[CodeChange(tx_index=1,new_code=b'\x60\x80\x60\x40...')]),AccountChanges(address=0xeeee...,# Coinbase
storage_changes=[],storage_reads=[],balance_changes=[BalanceChange(tx_index=0,post_balance=0x...05f5e1),# After tx 0 fees
BalanceChange(tx_index=1,post_balance=0x...0bebc2)# After tx 1 fees + reward
],nonce_changes=[],code_changes=[]),AccountChanges(address=0xffff...,# Factory contract
storage_changes=[SlotChanges(slot=b'\x00...\x01',# Storage slot 1
changes=[StorageChange(tx_index=1,new_value=b'\x00...\xdd\xdd...')# Deployed address
])],storage_reads=[],balance_changes=[],nonce_changes=[NonceChange(tx_index=1,new_nonce=5)],# After CREATE
code_changes=[])])
SSZ-encoded and compressed: ~400-500 bytes.
State Transition Function
Modify the state transition function to validate the block-level access lists:
defvalidate_bal(block):# Collect all state accesses and changes during block execution
collected_accesses={}# Process pre-execution system contracts (parent hashes, beacon roots)
pre_exec_tx_index=len(block.transactions)process_system_contracts_pre(block,collected_accesses,pre_exec_tx_index)# Execute transactions and track all state accesses
fortx_index,txinenumerate(block.transactions):execute_and_track(tx,tx_index,collected_accesses)# Process withdrawals after all transactions
post_exec_tx_index=len(block.transactions)+1forwithdrawalinblock.withdrawals:apply_withdrawal(withdrawal)track_withdrawal_changes(withdrawal,post_exec_tx_index,collected_accesses)# Process post-execution system contracts (EIP-7002, EIP-7251)
process_system_contracts_post(block,collected_accesses,post_exec_tx_index)# Build BAL from collected data
computed_bal=build_bal_from_accesses(collected_accesses)# Validate the BAL matches
assertblock.bal_hash==compute_bal_hash(computed_bal)defexecute_and_track(tx,tx_index,collected_accesses):# Execute transaction and get all touched addresses
touched_addresses=execute_transaction(tx)foraddrintouched_addresses:ifaddrnotincollected_accesses:collected_accesses[addr]={'storage_writes':{},# slot -> [(tx_index, value)]
'storage_reads':set(),'balance_changes':[],'nonce_changes':[],'code_changes':[]}# Record storage accesses
forslot,valueinget_storage_writes(addr).items():ifslotnotincollected_accesses[addr]['storage_writes']:collected_accesses[addr]['storage_writes'][slot]=[]collected_accesses[addr]['storage_writes'][slot].append((tx_index,value))# Record read-only storage accesses
forslotinget_storage_reads(addr):ifslotnotincollected_accesses[addr]['storage_writes']:collected_accesses[addr]['storage_reads'].add(slot)# Record balance change if any
ifbalance_changed(addr):collected_accesses[addr]['balance_changes'].append((tx_index,get_balance(addr)))# Record nonce change if any
ifnonce_changed(addr):collected_accesses[addr]['nonce_changes'].append((tx_index,get_nonce(addr)))# Record code change if any
ifcode_changed(addr):collected_accesses[addr]['code_changes'].append((tx_index,get_code(addr)))deftrack_withdrawal_changes(withdrawal,tx_index,collected_accesses):addr=withdrawal.addressifaddrnotincollected_accesses:collected_accesses[addr]={'storage_writes':{},'storage_reads':set(),'balance_changes':[],'nonce_changes':[],'code_changes':[]}# Record post-withdrawal balance
collected_accesses[addr]['balance_changes'].append((tx_index,get_balance(addr)))defbuild_bal_from_accesses(collected_accesses):account_changes_list=[]foraddr,dataincollected_accesses.items():storage_changes=[SlotChanges(slot=slot,changes=[StorageChange(tx_index=idx,new_value=val)foridx,valinsorted(changes)])forslot,changesindata['storage_writes'].items()]# Include pure reads and unchanged writes (excluded from storage_writes)
storage_reads=[slotforslotindata['storage_reads']ifslotnotindata['storage_writes']]account_changes_list.append(AccountChanges(address=addr,storage_changes=sorted(storage_changes,key=lambdax:x.slot),storage_reads=sorted(storage_reads,key=lambdax:x.slot),balance_changes=[BalanceChange(tx_index=idx,post_balance=bal)foridx,balinsorted(data['balance_changes'])],nonce_changes=[NonceChange(tx_index=idx,new_nonce=nonce)foridx,nonceinsorted(data['nonce_changes'])],code_changes=[CodeChange(tx_index=idx,new_code=code)foridx,codeinsorted(data['code_changes'])]))returnBlockAccessList(account_changes=sorted(account_changes_list,key=lambdax:x.address))defprocess_system_contracts_pre(block,collected_accesses,tx_index):# Pre-execution system contracts (parent hashes, beacon roots)
# EIP-2935: Historical block hashes
addr='0x0000000000000000000000000000000000000001'slot=(block.number-1)%8191record_storage_write(addr,slot,block.parent_hash,tx_index,collected_accesses)# EIP-4788: Beacon block root
addr='0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02'timestamp_slot=(block.timestamp%8192)+8192root_slot=block.timestamp%8192record_storage_write(addr,timestamp_slot,block.timestamp,tx_index,collected_accesses)record_storage_write(addr,root_slot,block.parent_beacon_block_root,tx_index,collected_accesses)defprocess_system_contracts_post(block,collected_accesses,tx_index):# Post-execution system contracts (withdrawals, consolidations)
# EIP-7002: Execution layer triggered withdrawals
ifis_prague_fork():track_contract_changes('0x00A3ca265EBcb825B45F985A16CEFB49958cE017',tx_index,collected_accesses)# EIP-7251: Consolidation requests
ifis_prague_fork():track_contract_changes('0x00b42dbF2194e931E80326D950320f7d9Dbeac02',tx_index,collected_accesses)deftrack_contract_changes(addr,tx_index,collected_accesses):"""Track all storage changes for a system contract"""forslot,valueinget_storage_changes(addr).items():record_storage_write(addr,slot,value,tx_index,collected_accesses)defrecord_storage_write(addr,slot,value,tx_index,collected_accesses):ifaddrnotincollected_accesses:collected_accesses[addr]={'storage_writes':{},'storage_reads':set(),'balance_changes':[],'nonce_changes':[],'code_changes':[]}ifslotnotincollected_accesses[addr]['storage_writes']:collected_accesses[addr]['storage_writes'][slot]=[]collected_accesses[addr]['storage_writes'][slot].append((tx_index,value))
The BAL MUST be complete and accurate. Missing or spurious entries invalidate the block.
Clients MUST validate by comparing execution-gathered accesses (per EIP-2929) with the BAL.
Clients MAY invalidate immediately if any transaction exceeds declared state.
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 execution. Omitting read values reduces size while maintaining parallelization benefits.
Storage values for writes: Post-execution values enable state reconstruction during sync without individual proofs against state root.
Overhead analysis: Historical data shows ~40 KiB average BAL size.