---
eip: 7928
title: Block-Level Access Lists
description: Enforced block access lists with state locations and post-transaction state diffs
author: Toni Wahrstätter (@nerolation), Dankrad Feist (@dankrad), Francesco D`Amato (@fradamt), Jochem Brouwer (@jochem-brouwer), Ignacio Hagopian (@jsign), Felipe Selmo (@fselmo), Rahul (@raxhvl), Stefan (@qu0b)
discussions-to: https://ethereum-magicians.org/t/eip-7928-block-level-access-lists/23337
status: Draft
type: Standards Track
category: Core
created: 2025-03-31
---

## Abstract

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](./eip-2930.md) 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([]))`.

```python
class Header:
    # Existing fields
    ...
    
    block_access_list_hash: Hash32 = keccak256(rlp.encode(block_access_list))
```

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`.

```python
# 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 = uint64  # 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; see [Gas Validation Before State Access](#gas-validation-before-state-access) for inclusion conditions)
    - Target addresses of `CREATE`/`CREATE2` if the target account is accessed
    - 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)
    - 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, regardless of whether the withdrawal amount is non-zero
    - Precompiled contracts when called or accessed

Addresses with no state changes **MUST** still be present with empty change lists.

Entries from an [EIP-2930](./eip-2930.md) access list **MUST NOT** be included automatically. Only addresses and storage slots that are actually touched or changed during execution are recorded.

### Block Access List Size Constraint

The block access list is constrained by the block gas limit rather than a fixed maximum number of items. The constraint is defined as:

```
bal_items <= block_gas_limit // ITEM_COST
```

Where:

- `bal_items = storage_keys + addresses`
- `ITEM_COST = 2000`

The `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](./eip-7981.md), the cheapest way to add an item to the BAL is a cold `SLOAD` at `COLD_SLOAD_COST` (2100, as defined in [EIP-2929](./eip-2929.md)). `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-2935.md), [EIP-4788](./eip-4788.md), [EIP-7002](./eip-7002.md), [EIP-7251](./eip-7251.md)) and withdrawal recipients ([EIP-4895](./eip-4895.md)) that do not consume block gas.

### Gas Validation Before State Access

State-accessing opcodes perform gas validation in two phases:

- **Pre-state validation**: Gas costs determinable without state access (memory expansion, base opcode cost, warm/cold access cost)
- **Post-state validation**: Gas costs requiring state access (account existence, [EIP-7702](./eip-7702.md) delegation resolution)

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](./eip-2929.md)):

| 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 regions

Post-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.

#### EIP-7702 Delegation

When a call target has an [EIP-7702](./eip-7702.md) 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

`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`.

### Ordering, Uniqueness 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 (ascending)
- **storage_reads**: Lexicographic by storage key
- **balance_changes, nonce_changes, code_changes**: By block access index (ascending)

The following uniqueness constraints **MUST** hold:

- Each address **MUST** appear exactly once in `BlockAccessList`.
- Each storage key **MUST** appear at most once in `storage_changes` per account.
- Each storage key **MUST** appear at most once in `storage_reads` per account.
- A storage key **MUST NOT** appear in both `storage_changes` and `storage_reads` for the same account.
- Each `block_access_index` **MUST** appear at most once per change list (`balance_changes`, `nonce_changes`, `code_changes`, and per-slot `StorageChange` list).

### 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.

#### Balance (`balance_changes`)

Record **post‑transaction** balances (`uint256`) for:

- Transaction **senders** (gas + value).
- Transaction **recipients** (only if `value > 0`).
- CALL/CALLCODE **senders** (value).
- CALL/CALLCODE **recipients** (only if `value > 0`).
- CREATE/CREATE2 recipients (only if `value > 0`).
- **COINBASE** (rewards + fees).
- **SELFDESTRUCT/SENDALL** beneficiaries.
- **Withdrawal recipients** (system withdrawals, [EIP-4895](./eip-4895.md)).

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 transfer recipients
- Calling a same-transaction SELFDESTRUCT on an address that had a zero pre-transaction balance

A zero-value block reward **MUST NOT** trigger a balance change in the block access list and **MUST NOT**, by itself, cause the recipient address to be included. The recipient **MUST** still be included if it is accessed for other reasons (e.g., transaction fee accounting).

#### Code

Track **post‑transaction runtime bytecode** for deployed or modified contracts, and **delegation indicators** for successful delegations as defined in [EIP-7702](./eip-7702.md).

#### Nonce

Record **post‑transaction nonces** for:

- EOA senders.
- Contracts that performed a successful `CREATE` or `CREATE2`.
- Deployed contracts.
- [EIP-7702](./eip-7702.md) authorities.

### Edge Cases (Normative)

- **COINBASE / Fee Recipient:** The COINBASE address follows the same inclusion rules as any other account. It is included when accessed (e.g., during transaction fee accounting) and excluded when not accessed (e.g., empty blocks with no relevant withdrawals).
- **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_reads` entry. 
- **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 nonce and code changes after any successful delegation set, update, or clear. If authorization fails after the authority address has been loaded and added to `accessed_addresses` (per [EIP-2929](./eip-2929.md)), 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).
- **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. The dequeue also reads up to 3 × `MAX_WITHDRAWAL_REQUESTS_PER_BLOCK` queue data slots (from slot 4 onward), which appear as storage_reads.
- **EIP‑7251 (consolidations):** Record system contract storage diffs of storage slots **0–3** (4 slots) after the dequeuing call. The dequeue also reads up to 4 × `MAX_CONSOLIDATION_REQUESTS_PER_BLOCK` queue data slots (from slot 4 onward), which appear as storage_reads.

### Engine API

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 list

**engine_newPayloadV5** validates execution payloads:

- Accepts `ExecutionPayloadV4` structure
- 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:

1. The EL receives the BAL in the ExecutionPayload
2. The EL computes `block_access_list_hash = keccak256(blockAccessList)` and includes it in the block header
3. The EL executes the block and generates the actual BAL
4. 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 stores it as a field in the SSZ `ExecutionPayload` container. For long-term storage, the full `blockAccessList` bytes MAY be pruned and replaced by the field's SSZ `hash_tree_root`, preserving the Merkle commitment within the `ExecutionPayload` tree.

**Retrieval methods** for historical BALs:

- `engine_getPayloadBodiesByHashV2`: Returns `ExecutionPayloadBodyV2` objects containing transactions, withdrawals, and `blockAccessList`
- `engine_getPayloadBodiesByRangeV2`: Returns `ExecutionPayloadBodyV2` objects containing transactions, withdrawals, and `blockAccessList`

The `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.

### State Transition Function

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.

```python 
def validate_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)
    assert rlp.encode(actual_bal) == execution_payload.blockAccessList

def execute_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)
    for i, tx in enumerate(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) + 1
    for withdrawal in block.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
    return build_bal(accesses)

def track_state_changes(tx, accesses, block_access_index):
    """Track all state changes from a transaction"""
    for addr in get_touched_addresses(tx):
        if addr not in accesses:
            accesses[addr] = {
                'storage_writes': {},  # slot -> [(index, value)]
                'storage_reads': set(),
                'balance_changes': [],
                'nonce_changes': [],
                'code_changes': []
            }
        
        # Track storage changes
        for slot, value in get_storage_writes(addr).items():
            if slot not in accesses[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)
        for slot in get_storage_reads(addr):
            if slot not in accesses[addr]['storage_writes']:
                accesses[addr]['storage_reads'].add(slot)
        
        # Track balance, nonce, code changes
        if balance_changed(addr):
            accesses[addr]['balance_changes'].append((block_access_index, get_balance(addr)))
        if nonce_changed(addr):
            accesses[addr]['nonce_changes'].append((block_access_index, get_nonce(addr)))
        if code_changed(addr):
            accesses[addr]['code_changes'].append((block_access_index, get_code(addr)))

def build_bal(accesses):
    """Convert collected accesses to BAL format"""
    bal = []
    for addr in sorted(accesses.keys()):  # Sort addresses lexicographically
        data = accesses[addr]
        
        # Format storage changes: [slot, [[index, value], ...]]
        storage_changes = [[slot, sorted(changes)] 
                          for slot, changes in sorted(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'])
        ])
    
    return bal
```

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.

### Concrete Example

Example block:

**Pre-execution:**

- EIP-2935: Store parent hash at block hash contract (`0x0000F90827F1C53a10cb7A02335B175320002935`)
- EIP-7002: Omitted for simplicity.

**Transactions:**

1. Alice (0xaaaa...) sends 1 ETH to Bob (0xbbbb...), checks balance of 0x2222...
2. Charlie (0xcccc...) calls factory (0xffff...) deploying contract at 0xdddd...

**Post-execution:**

- Withdrawal of 100 ETH to Eve (0xabcd...)
- EIP-7002 and EIP-7251 omitted for simplicity.

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):

```python
[
    # 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:

1. **Size vs parallelization**: BALs include all accessed addresses (even unchanged) for complete parallel IO and execution.

2. **Storage values for writes**: Post-execution values enable state reconstruction during sync without individual proofs against state root.

3. **Overhead analysis**: Historical data shows ~70 KiB average BAL size.

4. **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.

5. **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](../assets/eip-7928/bal_size_analysis.md). An updated analysis for a 60 million block gas limit can be found [here](../assets/eip-7928/bal_size_analysis_60m.md).

### 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.

### Early Rejection of Malicious BALs

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 accessed
- `G_remaining` = remaining block gas

The following invariant must hold:

```
G_remaining >= R_remaining * 2000
```

Where 2000 is the minimum gas cost for a storage read (via [EIP-2930](./eip-2930.md) 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

Copyright and related rights waived via [CC0](../LICENSE.md).
