Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-7928: Block-Level Access Lists

Enforced block-level access lists for parallalizing transaction validation

Authors Toni Wahrstätter (@nerolation), Dankrad Feist (@dankrad), Francesco D`Amato (@fradamt), Jochem Brouwer (@jochem-brouwer), Ignacio Hagopian (@jsign), Yoav Weiss (@yoavw), Alex Forshtat (@forshtat)
Created 2025-03-31
Discussion Link https://ethereum-magicians.org/t/eip-xxxx-block-level-access-lists/23337

Abstract

This EIP proposes adding Block-Level Access Lists (BALs). By including a complete list of all addresses and storage keys accessed during a block, along with their post-execution values for writes, we enable parallel disk reads and parallel transaction validation. This improves execution efficiency and accelerates block validation, potentially allowing for future gas limit increases.

Motivation

Currently, transactions without an explicit transaction access list cannot be efficiently parallelized, as the execution engine cannot determine in advance which addresses and storage slots will be accessed. While transaction-level access lists exist via EIP-2930, they are not enforced, making it difficult to optimize execution pipelines.

We propose enforcing access lists at the block level and shifting the responsibility of their creation to the block builder. This enables to efficiently parallelize both disk reads and transaction execution, knowing in advance the exact scope of storage interactions for each transaction.

The inclusion of post-execution values for writes in BALs provides an additional benefit for state syncing. Nodes can use these values to reconstruct state without processing all transactions, verifying correctness by comparing the derived state root to the head block’s state root.

BALs map transactions to (address, storage key, value) tuples and include balance diffs. This approach facilitates parallel disk reads and parallel execution, reducing maximum execution time to parallel IO + parallel EVM and improving overall network performance.

Specification

Block Structure Modification

We introduce three new components in the block body:

  1. A Block Access List (BAL) that maps transactions to accessed addresses and storage slots, transaction indices, and post-execution values for writes.
  2. Balance Diffs that track every address touched by value transfers along with the balance deltas.
  3. Nonces that record the pre-block nonces of contracts using CREATE or CREATE2 within the block.

SSZ Data Structures

# Type aliases
Address = ByteVector(20)
StorageKey = ByteVector(32)
StorageValue = ByteVector(32)
TxIndex = uint16
Nonce = uint64

# Constants; chosen to support a 630m block gas limit
MAX_TXS = 30_000
MAX_SLOTS = 300_000
MAX_ACCOUNTS = 300_000
MAX_CODE_SIZE = 24_576  # Maximum contract bytecode size in bytes

# SSZ containers
class PerTxAccess(Container):
    tx_index: TxIndex
    value_after: StorageValue # value in state after the last access

class SlotAccess(Container):
    slot: StorageKey
    accesses: List[PerTxAccess, MAX_TXS] # empty for reads

class AccountAccess(Container):
    address: Address
    accesses: List[SlotAccess, MAX_SLOTS]
    code: Union[ByteVector(MAX_CODE_SIZE), None]  # Optional field for contract bytecode

BlockAccessList = List[AccountAccess, MAX_ACCOUNTS]

# Balance Diff structures
BalanceDelta = ByteVector(12)  # signed, two's complement encoding

class BalanceChange(Container):
    tx_index: TxIndex
    delta: BalanceDelta  # signed integer, encoded as 12-byte vector

class AccountBalanceDiff(Container):
    address: Address
    changes: List[BalanceChange, MAX_TXS]

BalanceDiffs = List[AccountBalanceDiff, MAX_ACCOUNTS]

# Post-transaction nonce structures
class AccountNonce(Container):
    address: Address  # account address
    nonce_before: Nonce  # nonce value after block execution

NonceDiffs = List[AccountNonceDiff, MAX_TXS]

The BlockAccessList is a deduplicated list of accessed addresses. For each address, it MUST contain a list of accessed storage keys. For writes, each SlotAccess MUST contain an ordered list of transaction indices that accessed this key, and the final storage value after the last access. Transactions with writes that do not change the storage value MUST NOT contain a value_after. For contract deployments, the value_after MUST contain the runtime bytecode of the newly deployed contract.

The code field in AccountAccess is optional and MUST be present only when a contract is deployed. The code field MUST contain the runtime bytecode of the deployed contract.

For reads, each SlotAccess MUST contain an empty accesses list.

The BalanceDiffs structure tracks every address with a balance change, including transaction senders, recipients, and the block’s coinbase address. Touched accounts without balance changes MUST be omitted. Each entry MUST include the transaction index and the signed balance delta per address for each transaction. 12 bytes are sufficient to represent the total ETH supply.

The NonceDiffs structure MUST record the pre-transaction nonce values for all CREATE and CREATE2 deployer accounts and the deployed contracts in the block. This includes nonce increases that occur at the deployer contract even when deployments using CREATE or CREATE2 revert, as specified in EIP-7610.

State Transition Function

Modify the state transition function to validate the block-level access lists:

def state_transition(block: Block) -> None:
    computed_access_list = {}
    computed_balance_diffs = {}
    computed_nonce_diffs = {}
    computed_code_changes = {}

    for idx, tx in enumerate(block.transactions):
        # Execute transaction and track state accesses
        accessed_items, balance_changes, code_accesses = execute_transaction(tx)
        
        # Process storage accesses
        for (address, slot), is_write, value in accessed_items:
            if (address, slot) not in computed_access_list:
                computed_access_list[(address, slot)] = []
            
            access_entry = {"tx_index": idx}
            if is_write:
                access_entry["value_after"] = value
            
            computed_access_list[(address, slot)].append(access_entry)
        
        # Process balance changes
        for address, delta in balance_changes:
            if address not in computed_balance_diffs:
                computed_balance_diffs[address] = []
            computed_balance_diffs[address].append({"tx_index": idx, "delta": delta})
            
        # Process code accesses and changes
        for address, code in code_accesses:
            computed_code_changes[address] = code

    # Validate access list, balance diffs, and code changes
    if not validate_access_list(block.block_access_list, computed_access_list, computed_code_changes):
        raise InvalidBlock("Mismatch in block-level access list.")
        
    if not validate_balance_diffs(block.balance_diffs, computed_balance_diffs):
        raise InvalidBlock("Mismatch in balance diffs.")
        
    if not validate_nonce_diffs(block.nonce_diffs, computed_nonce_diffs):
        raise InvalidBlock("Mismatch in nonce diffs.")

The BAL MUST be complete and accurate. It MUST NOT contain too few entries (missing accesses) or too many entries (spurious accesses). Any missing or extra entries in the access list, balance diffs, or nonce diffs SHALL result in block invalidation.

Client implementations MUST compare the accessed addresses and storage keys gathered during execution (as defined in EIP-2929) with those included in the BAL to determine validity.

Client implementations MAY invalidate the block right away in case any transaction steps outside the declared state.

Rationale

BAL Design Choice

This design variant was chosen for several key reasons:

  1. Balance between size and parallelization benefits: BALs enable both parallel disk reads and parallel EVM execution while maintaining manageable block sizes. Since worst-case block sizes for reads are larger than for writes, omitting read values from the BAL significantly reduces its size. This approach still allows parallelization of both IO and EVM execution. While including read values would further enable parallelization between IO and EVM operations, analysis of historical data suggests that excluding them strikes a better balance between worst-case block sizes and overall efficiency.

  2. Storage value inclusion for writes: Including post-execution values for write operations facilitates state reconstruction during syncing, enabling faster chain catch-up. Unlike snap sync, state updates in BALs are not individually proved against the state root. Similar to snap sync, execution itself is not proven. However, validators can verify correctness by comparing the final state root with the one received from a light node for the head block.

  3. Balance and nonce tracking: Balance diffs and nonce tracking are crucial for correct handling of parallel transaction execution. While most nonce updates can be anticipated statically (based on sender accounts), contract creation operations (CREATE and CREATE2) can increase an account’s nonce without that account appearing as a sender. The nonce diff structure specifically addresses this edge case by tracking nonces for contract deployers and deployed contracts. For changing delegation under EIP-7702, the transaction type indicates that an authority’s nonce must be updated.

  4. Reasonable overhead with significant benefits: Analysis of historical blocks (random sample of 1000 blocks between 22,195,599 and 22,236,441) shows that BALs would have had an average size of around 57 KiB, with balance diffs adding only 9.6 KiB on average. This represents a reasonable overhead given the substantial performance benefits in block validation time.

  5. High degree of transaction independence: Analysis of the same block sample revealed that approximately 60-80% of transactions within a block are fully independent from one another, meaning they access disjoint sets of storage slots. This high level of independence makes parallelization particularly effective, as most transactions can be processed concurrently.

Block Size Considerations

Including access lists increases block size, potentially impacting network propagation times and blockchain liveness. Based on analysis of historical blocks:

  • Average BAL size over 1,000 blocks was around 57 KiB (SSZ-encoded, snappy compressed)
  • Average balance diff size was approximately 9.6 KiB
  • Worst-case BAL size for consuming the entire block gas limit with storage access operations would be approximately 0.93 MiB
  • Worst-case balance diff size would be around 0.12 MiB

These sizes are manageable and less than the current worst-case block size achievable through calldata.

Asynchronous Validation

Block execution can proceed with parallel IO and parallel EVM operations, with verification of the access list occurring alongside execution, ensuring correctness without delaying block processing.

Backwards Compatibility

This proposal requires changes to the block structure 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

Including comprehensive access lists, balance diffs and nonce values increases block size, potentially impacting network propagation times. However, as noted in the rationale section, the overhead is reasonable given the performance benefits, with typical BALs averaging around 67 KiB in total.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Toni Wahrstätter (@nerolation), Dankrad Feist (@dankrad), Francesco D`Amato (@fradamt), Jochem Brouwer (@jochem-brouwer), Ignacio Hagopian (@jsign), Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), "EIP-7928: Block-Level Access Lists [DRAFT]," Ethereum Improvement Proposals, no. 7928, March 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7928.