---
# System prepended metadata

title: A Deep Dive into EIP-7928 and Block-Level Access Lists (BAL)

---

# A Deep Dive into EIP-7928 and Block-Level Access Lists (BAL)

In this blog, we explore **Block-Level Access Lists (BAL)** introduced in [EIP-7928](https://eips.ethereum.org/EIPS/eip-7928) which is part of Glamsterdam Fork ([EIP-7773](https://eips.ethereum.org/EIPS/eip-7773)) and why they are being proposed for Ethereum. Today, Ethereum executes transactions sequentially because it does not know in advance which accounts and storage slots each transaction will access. This limitation leads to inefficient disk reads, prevents safe parallel execution, and increases overall validation time. BAL aims to solve this by requiring the block proposer to declare all state accesses (reads and writes) upfront, allowing validators to prepare the required data before execution and potentially run parts of the block in parallel.

We will walk through how BAL is structured, how it is generated and verified, and how it enables performance improvements like parallel disk reads and transaction validation. At the same time, we will also examine the tradeoffs and edge cases, such as handling malicious or incorrect BALs, why validators still need to execute transactions to verify correctness, and how mechanisms like gas-based checks help detect invalid blocks early. The goal is to build an intuitive and accurate understanding of how BAL works in practice and what problems it solves in Ethereum’s execution model. 

If you notice any technical mistakes, incorrect assumptions, or places where the explanation does not match how the protocol actually behaves, please feel free to point them out or leave a comment. Feedback is highly appreciated, as the aim is to keep this explanation as correct and useful as possible for others exploring this topic.

## Why Do we need BAL?

Ethereum today executes transactions one by one (sequentially). The core reason is simple: before a transaction runs, the system does not know exactly which data it will access. A transaction may read or modify account balances, interact with smart contracts, or access specific storage slots, but this information is only revealed during execution. Because of this uncertainty, Ethereum must assume that any transaction might depend on the result of a previous one. To ensure correctness, transactions are processed strictly in order.

This design leads to a clear performance issue. During execution, whenever a transaction needs data that is not already in memory, the client must fetch it from disk. Disk access is much slower than computation, so execution frequently pauses to wait for data. Since the system does not know in advance what data will be needed, it cannot efficiently load everything ahead of time. As a result, execution involves many small, unpredictable (random) disk reads, which significantly slows down block processing.

To make this more concrete, consider a simple example. Suppose:
- Tx1 needs data from Contract A (slot X)
- Tx2 needs data from Contract B (slot Y)

Right now, the client does:
“Run Tx1 $\rightarrow$ go to disk $\rightarrow$ load X $\rightarrow$ continue $\rightarrow$
Run Tx2 $\rightarrow$ go to disk $\rightarrow$ load Y $\rightarrow$ continue”

Instead of loading both X and Y together at the start, it keeps going back and forth to disk, which is slow.

Another big issue is parallel execution. Many transactions are actually independent. For example:
- Tx1 updates Alice’s balance
- Tx2 updates a DeFi contract
- Tx3 reads from another unrelated contract

These transactions do not interfere with each other and could, in theory, run at the same time. However, Ethereum cannot safely do this because it does not know beforehand whether two transactions might touch the same piece of state. If two transactions unknowingly modify the same storage slot and are executed in parallel, the final result could be incorrect. To avoid this risk, Ethereum processes all transactions sequentially, even when they are actually independent. This also makes it harder to optimize other parts of the system, such as computing the final state of the block, since changes are discovered step by step.

There have been attempts to improve this situation. For example, [**EIP-2930**](https://eips.ethereum.org/EIPS/eip-2930) introduced transaction-level access lists, allowing transactions to declare which addresses and storage slots they intend to use. However, these access lists are optional and not strictly enforced, meaning they can be incomplete or missing. Because of this, clients cannot rely on them for correctness or for safely scheduling execution. As a result, the fundamental problem remains: Ethereum still does not have a reliable way to know the full set of state accesses before execution begins.

### Why Storage Access Cannot Be Known Before Execution

At a high level, an Ethereum transaction runs code inside the EVM (Ethereum Virtual Machine). That code can make decisions while it runs—based on inputs, current state, and results of earlier operations. Because of this, **the exact storage slots a transaction will touch are only known during execution, not before**.

A simple reason is that **storage access can depend on dynamic values**. For example, a contract might compute a storage key like:

```
slot = hash(user_address, some_value)
```

Here, the exact slot depends on inputs (like `user_address`) and possibly other data read during execution from transaction. Until the transaction actually runs and computes this value, the system cannot know which slot will be accessed.

It becomes even more complex because contracts can call other contracts. For example:
```
Tx → Contract A → calls Contract B → calls Contract C
```
Each of these contracts may access their own storage, and which contracts get called can depend on runtime conditions. So a single transaction can trigger a chain of calls, and the full set of accessed storage locations is only revealed step by step during execution.

Another important point is that **execution depends on the current state**, which is itself changing as transactions are processed. For example:
- Tx1 updates some value at value X
- Tx2 reads that value X and decides what to do

So what Tx2 accesses can depend on what Tx1 changed. This means you cannot fully know Tx2’s behavior without executing Tx1 first.

### Why Is It Difficult to Optimize Final State Computation?

The final state of a block (often summarized as the **state root**) depends on all the changes made by transactions in that block. Since we do not know in advance which storage slots will be modified, we also cannot plan these updates ahead of time.

Today, the system works like this:
```
Execute Tx1 → update state  
Execute Tx2 → update state  
Execute Tx3 → update state  
...
```
As each transaction runs, it changes parts of the state, and these changes are applied one after another. Because we only discover which storage slots are touched during execution, the system has to update the state step by step in sequence.

This makes optimization difficult for two main reasons.

First, we cannot **prepare or group state updates in advance**. If we knew beforehand that Tx1, Tx2, and Tx3 all modify completely different storage slots, we could update those parts in parallel. But since we don’t know this upfront, we must assume they might overlap and handle them sequentially.

Second, computing the state root involves updating a data structure (a Merkle Patricia Trie) that represents all accounts and storage. These updates depend on which keys (accounts or storage slots) are changed. Since those keys are only known during execution, we cannot precompute or parallelize trie updates effectively.

A simple example helps here:
- Tx1 changes slot X
- Tx2 changes slot Y

If we knew this in advance, we could process updates for X and Y at the same time. But without that knowledge, we only discover:
```
Run Tx1 → oh, slot X changed  
Run Tx2 → now slot Y changed  
```
So everything happens step by step instead of in parallel.

### What Limitations Exist with EIP-2930?

[EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) introduced the idea of transaction-level access lists, where a transaction can declare in advance which addresses and storage slots it plans to access. At first glance, this looks like a solution to the main problem we discussed - if we already know what a transaction will touch, we could prepare data ahead of time and even try parallel execution. However, in practice, this approach has several important limitations.

The biggest issue is that these access lists are **optional and not enforced for correctness**. A transaction may include an access list, but it is not required to be complete. If a transaction accesses some storage that was not listed, the transaction still executes normally - it just pays a higher gas cost for “cold access.” This means clients cannot rely on these lists as a trusted source of truth. Since correctness is not tied to the access list, validators cannot safely rely on it for execution scheduling or parallelization.

Another limitation is that access lists are defined **per transaction, not per block**. Even if every transaction provided a perfect access list (which is not guaranteed), the client would still have to combine all of them and reason about dependencies across the entire block. This becomes complicated because transactions can interact with each other. For example, one transaction might change a storage slot that another transaction later reads. Without a complete and enforced view of all accesses at the block level, it is still difficult to safely determine which transactions are independent.

There is also a practical challenge: generating a correct access list is not easy. Since smart contract execution is dynamic, a user or wallet would often need to simulate the transaction beforehand to know exactly which storage slots will be accessed. Even then, edge cases like conditional logic or contract calls can make the access list incomplete. Because of this complexity, and because there is little penalty for omitting it, many transactions simply do not include access lists at all, reducing their usefulness in practice.

To understand this better, consider a simple example. Suppose a transaction interacts with a DeFi contract and accesses storage slot X and Y. Ideally, it would include both in its access list. But if it forgets to include slot Y, the transaction will still run successfully - it will just pay more gas. From the network’s perspective, nothing breaks. However, a validator cannot rely on this access list for planning execution, because it might be missing important information.

**Unlike EIP-2930 access lists, BAL is required to be complete and is indirectly consensus-validated via its hash in the block header.**

### What is EIP-7928 (Block-Level Access Lists)?

EIP-7928 introduces **Block-Level Access Lists (BAL)**, a new way for Ethereum blocks to declare, upfront, all the state that will be accessed during their execution. Instead of discovering which accounts and storage slots are used while running each transaction, the block itself now carries a complete list of these accesses. This shifts Ethereum from a “figure it out during execution” model to a “know before execution starts” model, which is essential for improving performance.

At a high level, BAL introduces a structured list that records **which accounts are touched and which storage locations are read or written during the entire block**, along with the resulting state changes. This information is organized per account and includes things like storage updates, storage reads, balance changes, nonce updates, and contract code changes. Importantly, it also records when (at which transaction index) these changes happen, allowing clients to understand how state evolves across the block.

To make this information part of block validation, EIP-7928 adds a new field to the block header called `block_access_list_hash`. This field contains the hash of the full Block Access List. The actual BAL itself is not stored directly in the block body; instead, it is transmitted alongside the block through the execution layer’s Engine API as part of the execution payload. This design keeps the block header compact while still allowing clients to verify that the provided access list is correct by recomputing and comparing the hash after execution.

### Structure of Block-Level Access Lists

A Block-Level Access List (BAL) is organized in a structured and deterministic way so that every client can interpret it the same way. At the top level, a BAL is simply a list of accounts that were accessed during block execution. For each account, all its interactions are grouped together into a structure called **AccountChanges**. You can think of this like a per-account “summary” of everything that happened to that account during the block.

Each **AccountChanges** entry contains multiple categories of information. First, there are **storage_changes**, which record updates to storage slots. These are stored per slot, and for each slot, we keep a list of changes showing at which point in the block the value changed and what the new value became. This allows us to track how a storage slot evolves across transactions.

Next, there are **storage_reads**, which are storage slots that were accessed but not modified. These represent read-only accesses. An important detail is that reads are only recorded if the slot is not written to later; if a slot is both read and written, it is treated as a write and included in **storage_changes** instead. This avoids duplication and keeps the structure consistent.

Then we have **balance_changes**, **nonce_changes**, and **code_changes**, which track updates to an account’s balance, nonce, and contract code respectively. Each of these is recorded as a list of changes, where each entry tells us the state of that field after a specific point in execution. For example, **balance_changes** store the post-transaction balance after each transaction that modified it.

A key concept that ties all of this together is the BlockAccessIndex. This is an index that tells us when a particular change happened within the block. The indexing is defined as:
- `0` for pre-execution system operations
- `1 ... n` for transactions (in block order)
- `n + 1` for post-execution operations (like withdrawals)

It is important to note that not all state changes come from user transactions. Some changes are introduced by **system-level operations**, such as protocol-defined contract calls that happen before or after transaction execution (for example, updates from system contracts or withdrawals). The BlockAccessIndex ensures that these system-level changes are also captured in a consistent and ordered way, separate from regular transactions.

So, if a storage slot has a change recorded as `[2, value]`, it means that after transaction $2$, the slot’s value became `value`. This gives a clear timeline of how state evolves across the block.

The way changes are recorded follows a few important rules to ensure consistency. Each account appears only once in the BAL. Storage slots are uniquely listed, and their changes are ordered by BlockAccessIndex. Similarly, balance, nonce, and code changes are recorded in ascending order of when they occurred. This strict ordering ensures that all clients can reconstruct the same view of state transitions without ambiguity.

To build intuition, imagine a simple case where a contract’s storage slot `X`
is updated twice in a block:
- Tx1 sets `X = 10`
- Tx3 sets `X = 20`

In the BAL, this would appear as:
```
slot X --> [[1, 10], [3, 20]]
```
This tells us exactly how the value changed over time, without needing to re-run the transactions.

To ensure that all clients interpret and verify the BAL in exactly the same way, the EIP also defines strict rules for **ordering and uniqueness**. Accounts must be sorted in lexicographic order, storage slots must be sorted by their keys, and all changes must be ordered by their BlockAccessIndex. Additionally, each address and storage key must appear only once in their respective sections, preventing duplicates. These rules are critical because they guarantee that the BAL has a deterministic encoding. Without strict ordering and uniqueness, different clients could produce slightly different representations of the same data, which would break hash comparison and lead to inconsistent validation.

**These ordering rules are consensus - critical, because even a different ordering of the same data would produce a different hash, causing the block to be rejected.**

### What Must Be Included in a Block-Level Access List (BAL)
A Block-Level Access List must include **every account that is accessed during the execution of a block**, not just those that change state. The key idea is simple: if the EVM touches an address in any meaningful way - whether by reading from it, writing to it, or interacting with it—that address must appear in the BAL. This ensures that the list fully represents all state dependencies of the block.

This includes all accounts that undergo **state changes**, such as storage updates, balance transfers, nonce increments, or code changes. But it also includes accounts that are **only read or interacted** with, even if nothing about their state actually changes. For example, when a transaction checks the balance of an address, reads its code, or queries its code hash, that address is still considered “accessed” and must be included.

More specifically, any address that is the target of EVM operations like **CALL**, **CALLCODE**, **DELEGATECALL**, or **STATICCALL** must be included. This is true even if the call fails or reverts, as long as the execution reached the point where the target address was accessed. Similarly, when a contract is created using **CREATE** or **CREATE2**, the resulting contract address must be included if it is accessed during execution. In the case of **SELFDESTRUCT**, both the contract being destroyed and the beneficiary receiving the funds must be included, since their balances are affected.

**Precompiled contracts** (built-in contracts like cryptographic functions) must also be included whenever they are called. Even though they are part of the protocol and not regular user contracts, they are still treated as accessed addresses in the execution process.

An important detail is that even **“no-change” accesses must be included**. That means if an address is touched during execution but ends up with no changes to its state, it must still appear in the BAL with empty change lists. For example, if a transaction performs a **STATICCALL** to a contract just to read some data, and no state is modified, that contract’s address must still be listed. This is crucial because BAL is not only about tracking changes - it is about declaring all dependencies of execution. Missing such an address would mean the BAL is incomplete and could lead to incorrect assumptions about which parts of the state are needed.

Another important rule is that **state must actually be accessed to be included in the BAL**. Before any state access happens, the EVM first performs a “pre-state gas validation” step to check whether there is enough gas to execute the operation. If this check fails, the state is never accessed at all. As a result, such attempted accesses must **not** be included in the BAL. This ensures that the BAL only contains state that was truly accessed during execution and prevents incorrect inclusion of data from operations that never actually reached the state.

A subtle example of this rule appears with the **SSTORE** operation. Although SSTORE normally involves reading the current storage value to calculate gas costs, this read only happens if certain gas conditions are met. If the operation is executed under a limited gas stipend (for example, when there is not enough gas available within a call), the read is never performed. In such cases, the storage slot must not be included in the BAL, because it was never actually accessed. This ensures that even implicit or conditional accesses are only recorded when they truly occur.

Another advanced case arises with delegation mechanisms as defined in **EIP-7702**. When a transaction interacts with an account that uses delegation, execution may be redirected to a delegated address. However, this delegated address is only included in the BAL if it is actually accessed during execution. If delegation resolution fails before the delegated account is accessed, then the delegated address must not appear in the BAL. This ensures that even in more complex execution flows, the BAL strictly reflects real state access and does not include speculative or unused addresses.

To understand this intuitively, think of BAL as a complete “footprint” of the block on the state. Even if you step on a surface without leaving a mark, that step still matters if someone is trying to trace where you walked. However, if you never actually step on the surface (because you stopped before reaching it), it should not be counted at all. Similarly, BAL includes all real accesses - even those with no changes - but excludes anything that was never actually executed.

**The BAL must be complete - any missing accessed address or storage slot will cause a mismatch during validation, making the block invalid.**

### Edge Cases and Special Rules

While the general rules define what should be included in the BAL, the EIP also specifies several important edge cases to ensure correctness in less obvious situations. These rules make sure that the BAL always reflects what actually happened during execution, even when the outcome is unusual.

One important case is **reverted transactions**. Even if a transaction ultimately fails and its state changes are rolled back, any accounts and storage slots that were accessed before the *revert* must still be included in the BAL. This is because the EVM did touch those parts of the state during execution, and BAL is meant to capture actual access, not just final state changes.

Another case is **zero-value transfers**. If a transaction sends zero ETH to an address, that recipient address must still be included in the BAL. Even though no balance change occurs, the address is still part of the execution path and therefore represents a dependency that must be recorded.

For **SELFDESTRUCT**, the behavior is slightly more involved. When a contract is destroyed, both the contract itself and the beneficiary receiving its balance must be included. If the contract had a positive balance, the transfer to the beneficiary must be recorded as a balance change. Additionally, any storage that was accessed before the destruction must still be included, typically as storage reads.

**Precompiled contracts** (built-in system contracts used for operations like cryptography) must always be included when they are accessed. If they receive value during execution, this is recorded as a balance change; otherwise, they appear in the BAL with empty change lists. Even though they are part of the protocol, they are still treated like accessed addresses.

The **COINBASE address** (the block proposer or fee recipient) must also be handled carefully. It must be included in the BAL if it experiences any state change, such as receiving transaction fees or rewards. In cases where the reward is zero, it may still be included as an accessed address depending on how it is used during execution.

Finally, system-level operations that occur before or after transaction execution (such as protocol-defined contract calls or withdrawals) must also be reflected in the BAL using the appropriate BlockAccessIndex values. These ensure that the BAL captures the full lifecycle of block execution, not just user transactions.

Together, these edge cases ensure that the BAL remains complete and accurate in all scenarios, including failures, no-op operations, and system-level behavior.

### How BAL is Built and Verified
The lifecycle of a Block-Level Access List (BAL) has two sides: how it is **built by the proposer** and how it is **verified by validators**. The key idea is that the proposer declares what will be accessed, but validators must independently confirm that this declaration matches reality.

When a proposer builds a block, it executes all transactions and **records every state access and change during execution**. This includes which accounts were touched, which storage slots were read or written, and how balances, nonces, and code changed over time. From this information, the proposer constructs the BAL in the required structured format. Once built, the proposer computes a hash of this BAL and places it in the block header (`block_access_list_hash`).

It is important to note that **only the hash of the BAL is part of the block header, not the BAL itself**. The full BAL is transmitted separately along with the block via the execution payload (through the Engine API). This means that consensus commits to the hash, while the actual BAL content must be independently verified by validators.

On the validator side, the process is intentionally independent. The validator does not trust the proposer’s BAL directly. Instead, while executing the block, the validator **tracks all actual accesses on its own** and builds its own version of the BAL. This locally generated version is often called the “**virtual BAL**”. It is not something received over the network—it is reconstructed from real execution.

After execution finishes, the validator compares its virtual BAL with the proposer-provided BAL. This is done by encoding the virtual BAL in the same format and checking whether it exactly matches the one received (or equivalently, whether their hashes match the `block_access_list_hash` in the header). If they match, the block is valid. If there is any mismatch—missing entries, extra entries, or incorrect ordering—the block is considered invalid and is rejected.

An important point is that **execution correctness is based on the actual state, not by the BAL**. The BAL does not control what the EVM reads or writes; it only declares what is expected to happen. So even if the proposer provides an incorrect or malicious BAL, the validator will still execute transactions correctly and detect the mismatch afterward.

To build intuition, think of the BAL like a “declared plan” and the virtual BAL like a “record of what actually happened.” The proposer submits the plan, but the validator independently runs the process and checks whether the plan was accurate. If the plan and reality differ, the block is rejected.

In addition to building and verifying BALs, the execution layer is also required to **store them for a certain period of time**. Specifically, clients must retain BAL data for at least the duration of the weak subjectivity period (approximately 3533 epochs). This ensures that nodes which go offline for a limited time can later re-sync and verify blocks without needing to fully re-execute all transactions from scratch, improving reliability and sync efficiency.

### Two BALs Concept
In the BAL design, there are effectively **two versions of the access list** involved during block processing. The first is the **proposer’s BAL**, which is the access list constructed by the block proposer and sent along with the block. The second is the **validator’s BAL**, often called the virtual BAL, which is independently generated by each validator while executing the block.

The proposer’s BAL is built during block creation by tracking all state accesses and changes as transactions are executed. This version is then included (via the execution payload) and its hash is committed in the block header. It represents what the proposer claims will happen during execution. However, this claim is not trusted blindly.

On the validator side, a separate BAL is created during execution. As the validator runs each transaction, it records the actual accounts and storage locations accessed, along with the resulting state changes. This reconstructed version - the virtual BAL - is based entirely on real execution, not on the proposer’s input. In other words, the validator observes what actually happens rather than trusting what was declared.

Both versions are needed because they serve different purposes. The proposer’s BAL is used to enable optimizations such as prefetching and parallel processing, since it provides an upfront view of state access. The validator’s BAL, on the other hand, is used to **verify correctness**. Without this independent reconstruction, a malicious proposer could provide incorrect or incomplete access information and potentially mislead execution.

Correctness is ensured by comparing these two versions after execution. The validator encodes its virtual BAL and checks that it exactly matches the proposer-provided BAL (or equivalently, that their hashes match the value in the block header). If there is any difference - such as a missing accessed address, an incorrect storage slot, or a mismatch in ordering - the block is considered invalid and rejected.

### How BAL Enables Performance Improvements

The main benefit of Block-Level Access Lists (BAL) comes from one key idea: the system knows in advance which parts of the state will be accessed during block execution. This allows clients to plan work ahead of time instead of reacting during execution, improving overall efficiency.

First, BAL enables **parallel disk reads (prefetching)**. Since all required accounts and storage slots are known upfront, clients can load this data before execution begins and do so in parallel. This avoids repeated disk access during execution and reduces waiting time caused by on-demand data fetching.

Second, BAL enables **parallel transaction execution**. By knowing which transactions access which parts of the state, clients can identify transactions that do not overlap and safely execute them at the same time. This allows better utilization of available CPU resources compared to strictly sequential execution.

Third, BAL improves **state update efficiency**. With a complete view of which parts of the state will change, clients can organize and apply updates more effectively. This makes it easier to optimize how the final state is computed and reduces unnecessary sequential processing.

BAL also enables **execution-less or reduced-execution state updates in specific scenarios**, such as syncing or state reconstruction. Since it records both accesses and resulting state changes, clients can reuse this structured information instead of fully re-executing transactions in some contexts. However, full correctness still requires execution, so this is an optimization opportunity rather than a complete replacement.

Another important aspect is that **validation work overlaps with execution**. As transactions are executed, clients simultaneously track accesses and build the virtual BAL. This means validation is not a separate blocking step—it happens alongside execution and data loading. In practice, disk I/O, execution, and validation can all overlap, improving overall throughput.

In simple terms, BAL allows the system to plan ahead, reduce waiting on disk, and make better use of available hardware, resulting in faster and more efficient block processing.

### The Big Tradeoff: Performance vs Safety

Block-Level Access Lists (BAL) improve performance by giving validators advance information about state access, but they introduce a clear tradeoff: **this information cannot be fully trusted.**

Because the BAL is provided by the proposer, it may be incorrect or incomplete. For this reason, it is treated only as a **performance hint**, not a source of truth. Validators still rely on actual execution to determine what really happens in the block, and correctness is always based on that execution.

The tradeoff appears in how BAL is used. Clients may use it to prefetch data or schedule execution more efficiently, but if the BAL is wrong, this can lead to **wasted work** - for example, loading unnecessary data or executing transactions under incorrect assumptions. The issue is only discovered after execution when the actual accesses are compared with the BAL.

This design is intentional. The system prioritizes **safety over efficiency**, ensuring that incorrect BALs never lead to an incorrect state. At worst, they cause extra computation, not incorrect results. At the same time, when the BAL is correct (which is expected in normal conditions), it enables significant performance improvements.

In short, BAL introduces a controlled tradeoff: **use untrusted information to gain speed, but always verify it using real execution to guarantee correctness.**

**In this sense, BAL follows an optimistic design: the system assumes that proposers provide useful information to improve performance, but always verifies it afterward to ensure correctness.**

### Parallel Execution with BAL

Block-Level Access Lists (BAL) allow validators to **schedule transactions based on their actual state access**, instead of assuming that every transaction depends on the previous one.

In practice, the validator uses the BAL to build a **dependency view of the block**. Each transaction is associated with the accounts and storage slots it accesses. Using this information, the validator checks whether two transactions touch overlapping state. If they do not overlap, they can be executed in parallel. If they do overlap - especially when at least one of them writes to the same location - they must be executed in order.

You can think of this like scheduling tasks:
- If two tasks use completely different resources, they can run at the same time.
- If they use the same resource, they must wait for each other.

For example, if one transaction accesses storage slot `X` and another accesses slot `Y`, and `X ≠ Y`, they can be scheduled in parallel. But if both access slot `X`, then the validator must enforce ordering to avoid conflicts.

BAL makes this scheduling possible because it provides a **block-level view of all accesses**, allowing the validator to determine dependencies *before execution begins*. Based on this, the validator can construct an execution plan where independent transactions are grouped and run concurrently, while dependent ones are executed sequentially in the correct order.

When the BAL is correct, this scheduling leads to efficient execution across multiple CPU cores while still preserving the exact same final state as sequential execution.

**It is important to note that BAL only provides a proposed dependency view. Clients must treat BAL-based scheduling as speculative and be prepared to handle conflicts if the declared dependencies are incorrect.**

### What Happens if BAL is Malicious?

Since the Block-Level Access List (BAL) is provided by the proposer, it can be incorrect or even malicious. A simple example is when two transactions actually access the **same storage slot**, but the BAL claims they access different slots. This makes them look independent when they are not.

If a validator uses this BAL for scheduling, it may run those transactions in parallel. During execution, both transactions will still access the real state (the same slot), so their operations can interfere with each other. In simple terms, the validator started with a **wrong assumption about independence**, which can lead to incorrect intermediate execution behavior.

The important point is what happens next. As the validator executes the block, it keeps track of the actual accesses that occur. By the end of execution, it has a complete and correct view of which slots and accounts were truly accessed.

At that point, the validator compares this with the BAL provided by the proposer. Since the proposer hid a dependency, the two will not match. This mismatch reveals that the BAL was incorrect.

So even though execution may have been scheduled incorrectly, the issue is detected after execution, and the block is rejected. This ensures that incorrect or malicious BALs never lead to an incorrect final state - they only cause wasted work, not incorrect results.

You can think of it like following a wrong map: you may start moving in the wrong direction, but once you check your actual path against the map, the mistake becomes obvious.

### How Validators Detect Incorrect BAL

Validators detect an incorrect Block-Level Access List (BAL) by **reconstructing what actually happened during execution** and comparing it with what the proposer declared. The key idea is that validators do not rely on the provided BAL for correctness—they independently observe all state accesses while running the block.

As the validator executes each transaction, it records every account and storage slot that is actually accessed, along with any changes to balances, nonces, code, or storage values. This process builds what is often called the **virtual BAL** - a local, execution - derived version of the access list. It is created purely from real execution behavior, not from the proposer’s input.

A critical point is that **execution always uses the real state**, not the BAL. The EVM follows its normal rules: when a transaction needs a storage slot or account, the client fetches it from the actual state (from memory or disk if needed). The BAL does not restrict or guide what can be accessed; it is only a declaration of what is expected. This guarantees that execution remains correct even if the BAL is wrong or incomplete.

The mismatch between the proposer’s BAL and the validator’s virtual BAL can only be fully detected **after execution completes**. This is because the complete set of accesses - especially reads - depends on the entire execution of the block. Some accesses are only known after all transactions have run, and certain distinctions (like whether a slot is ultimately classified as a read or a write) depend on the full sequence of operations. Because of this, validators must finish execution before they can construct the final, ordered BAL and compare it.

Once execution is done, the validator encodes its virtual BAL and compares it with the proposer - provided BAL (or equivalently checks against the `block_access_list_hash` in the block header). If there is any difference—missing entries, extra entries, or incorrect ordering - the block is invalid and rejected.

### Important Clarification: BAL vs Actual Execution

A key point to understand is that **BAL does not control execution**. It is only a declaration of expected accesses provided by the proposer. The EVM always executes transactions using the **real state and normal rules**, independent of what the BAL says.

In fact, a client could **completely ignore the BAL and still execute the block correctly**, though less efficiently. This shows that BAL is not required for correctness - it is an optimization layer that improves performance but is not needed to produce the correct result.

If the BAL is incomplete or incorrect, execution still proceeds correctly. Any required data that is not already loaded can be fetched during execution as usual. This means BAL is **not required for correctness** - it only helps improve performance.

Because of this, **missing or incorrect prefetching does not make a block invalid**. The only thing that matters for validity is whether the actual accesses during execution match the BAL after execution. If they do not match, the block is rejected at that point.

A simple way to think about this is: BAL is like a plan, but execution follows reality. Even if the plan is wrong, the system still produces the correct result and later checks whether the plan was accurate.

### Why Writes Can Be Validated Early but Reads Cannot

In a Block-Level Access List, there is an important difference between **writes** (state changes) and **reads** (state accesses without change), and this difference affects when they can be validated. A write happens when a transaction changes something in the state - like updating a storage slot, modifying a balance, or deploying code. A read happens when a transaction only looks at existing data, such as checking a balance or loading a storage value, without changing it.

Writes can be validated early because they are **directly tied to specific transactions** and produce observable changes at that moment. When a transaction executes and modifies a storage slot or balance, the validator immediately knows that this change occurred and can record it with the corresponding transaction index. Since the change is explicit and final at that point in execution, it can be checked and tracked incrementally as each transaction runs.

Reads, on the other hand, depend on the **entire block execution**, not just a single transaction. This is because of how BAL defines reads: a storage slot is recorded as a read only if it is accessed but **not written to later in the block**. If a slot is first read and then later written, it is treated as a write and only appears in the storage_changes section, not in storage_reads. This means whether something counts as a “read” in the final BAL depends on what happens in later transactions.

For example, consider a simple case:
- Tx1 reads storage slot X
- Tx3 later writes to slot X

Even though Tx1 read slot X, the final BAL will list X under **writes**, not reads. So when the validator executes Tx1, it cannot immediately decide whether this access should be recorded as a read—it must wait to see if any later transaction writes to the same slot.

This is why reads can only be fully validated **after all transactions in the block have been executed**. Only then does the validator have the complete picture of which slots were read-only and which were ultimately written.

**This is why read validation is fundamentally harder than write validation and requires full block execution.**

### The Adversarial BAL Problem (Phantom Reads Attack)

BAL improves performance by declaring all state accesses upfront, but this also opens a subtle attack surface. A malicious proposer can include fake or **“phantom” storage reads** in the BAL—entries that are declared as accessed but are never actually touched during execution. These fake reads do not break correctness directly, but they are designed to **mislead validators into doing unnecessary work**.

The attack works by declaring a large number of storage reads in the BAL. Since validators use BAL to prefetch data, they will start loading all those storage slots from disk into memory before execution. However, during actual execution, the transactions never access those slots. This means the validator has done a significant amount of useless I/O work based on incorrect information provided by the proposer.

The reason validators cannot detect this early is due to how reads are defined and validated. As discussed earlier, whether a storage slot is truly a “read” depends on the **entire block execution**. A slot might appear as a read initially but could later be written, or might not be accessed at all. Because of this, validators cannot immediately verify whether a declared read is valid - they must execute the full block to know for sure. This delay creates a window where fake reads cannot be distinguished from real ones.

As a result, the validator ends up performing unnecessary operations, especially **disk reads and data prefetching**, which are expensive compared to computation. In the worst case, a malicious block could force validators to load a large amount of irrelevant data, increasing latency and resource usage. Even though the block will eventually be rejected once the mismatch is detected, the cost of processing it is still incurred.

To understand this simply, imagine someone gives you a list of 100 items to prepare before cooking, but in reality, only $10$ of them are needed. You spend time collecting all $100$ items, only to realize later that most of them were never used. The final result is still correct, but a lot of effort was wasted.

### Solution: Gas-Budget Feasibility Check

To defend against the “phantom reads” problem, EIP-7928 introduces a simple but effective idea: use **gas as a physical limit** on how many storage reads can actually happen. In Ethereum, every operation costs gas, and reading from storage is not free. This means there is a hard upper bound on how many reads a block can perform based on the gas available.

The key observation is that **each storage read has a minimum gas cost**. Even in the cheapest case (using access lists and warm reads), a storage read still costs at least a small fixed amount of gas. Because of this, if a BAL declares a large number of reads, the block must have enough gas to actually perform them. Otherwise, the declaration is impossible.

This leads to a simple feasibility check during execution:

```
G_remaining ≥ R_remaining × 2000
```
Here:
- `G_remaining` is the gas left in the block
- `R_remaining` is the number of declared storage reads that have not yet been observed
- `2000` is a safe lower bound for the cost of a storage read

The logic is straightforward. As the validator executes transactions, it keeps track of how many declared reads are still “unaccounted for.” At the same time, it knows how much gas is left. If at any point the remaining gas is **not enough** to perform the remaining reads, then the BAL must be lying—because it is mathematically impossible to execute that many reads with the available gas.

For example, suppose:
- BAL declares $100$ reads
- Only $20$ have been observed so far $\rightarrow$ $80$ remaining
- Remaining gas = $100,000$

Minimum gas needed:
```
80 × 2000 = 160,000
```
Since $100,000 \lt 160,000$, it is impossible for the block to perform those remaining reads. At this point, the validator can **reject the block early**, without finishing execution.

This mechanism works because gas rules are enforced by the protocol itself - no transaction can bypass them. So if the declared reads exceed what is physically possible under the gas limit, the BAL must be invalid.

### Early Rejection of Malicious Blocks

EIP-7928 includes a practical safeguard so validators don’t waste too much work on bad BALs: **early rejection**. The idea is to check, during execution, whether the remaining declared reads in the BAL are even possible given the gas left in the block. If they are not possible, the block can be rejected immediately without finishing execution.

In practice, as a validator executes transactions, it keeps two simple counters: how much **gas is left** in the block and how many **declared reads are still unobserved**. Using the feasibility rule from the EIP (remaining gas must be enough to pay for the remaining reads), the validator periodically checks whether:
```
G_remaining ≥ R_remaining × 2000
```
If this condition fails at any point, it means the block is claiming more reads than could ever be executed with the remaining gas. At that moment, the validator can **stop execution and reject the block early**, instead of continuing to process all transactions.

This mechanism is important because it **prevents worst-case abuse**. Without it, a malicious proposer could declare a very large number of fake reads, forcing validators to prefetch large amounts of data and execute the entire block before discovering the mismatch. With early rejection, such blocks are detected and dropped much sooner, significantly reducing wasted disk I/O and computation.

These checks are not required to run after every single transaction; they are typically performed periodically during execution (for example, every few transactions). This keeps the overhead of checking low while still allowing validators to catch obviously invalid BALs early enough to avoid most unnecessary work.

### Real Client Behavior (e.g., Geth Model)

In practice, clients like Geth implement BAL-aware execution using a **parallel worker model** while still preserving Ethereum’s correctness rules. The idea is to use multiple threads to execute transactions where possible, while a central component keeps track of results and state accesses.

Execution is typically handled by **worker threads**. These workers pick transactions (or groups of transactions) and execute them using the EVM. When BAL is available, it can be used as a hint to schedule transactions that appear independent onto different workers. This allows multiple transactions to run at the same time, improving CPU utilization. However, even in this parallel setup, execution still uses the real state, and any conflicts or dependencies are ultimately resolved according to Ethereum’s rules.

Alongside this, there is a **result handler (or coordinator)** that collects what each worker did. As transactions execute, the client records all accessed accounts, storage reads, and state changes. These observations are aggregated into a structure that will later form the validator’s **virtual BAL**. The result handler ensures that all accesses are tracked in a consistent and ordered way, even if execution happened across multiple threads.

An important detail is that **read validation is deferred**. As discussed earlier, whether something is classified as a read depends on the full execution of the block (for example, a slot read early might later be written). Because of this, clients do not finalize read entries immediately. Instead, they collect access information during execution and only finalize the exact BAL structure after all transactions have completed.

To visualize this, imagine a small system:

- Multiple workers are cooking different dishes in parallel (executing transactions)
- A supervisor notes every ingredient used (tracking accesses)
- Only after all dishes are finished does the supervisor finalize the complete list

This approach aligns with the EIP design: use BAL to **enable parallelism and efficiency**, but rely on actual execution and post-processing to ensure correctness. Even though execution can be parallelized internally, the final result—state changes and the reconstructed BAL—remains deterministic and verifiable across all clients.

### Execution Model Summary

At a high level, the BAL-based execution flow separates **performance optimization** from **correctness verification**.

When a validator receives a block, it also receives the BAL. This BAL is used as a **hint** to prepare execution—for example, by preloading required data and planning how transactions can be scheduled.

The validator then executes the transactions using the **real state**, while simultaneously tracking all actual accesses and building its own internal view (the virtual BAL).

After execution completes, the validator compares what actually happened with what was declared in the BAL. If they match, the block is valid. If not, the block is rejected.

In simple terms:

Use BAL to prepare and optimize
Execute using real state
Verify by comparing expected vs actual

This separation ensures performance gains without compromising correctness.

### Limitations and Open Question

While BAL enables strong performance improvements, it comes with some limitations and open questions.

The main limitation is that **BAL cannot be fully trusted for scheduling**. Since it is provided by the proposer, it may be incorrect. This means clients must be careful in how aggressively they rely on it for parallel execution.

In adversarial cases, incorrect BALs can lead to **wasted work**, such as unnecessary prefetching or suboptimal scheduling. However, correctness is not affected, since validation always happens after execution.

There are also **implementation challenges for clients**. Supporting BAL requires additional logic for scheduling, tracking accesses, and building the virtual BAL in a consistent way. This increases system complexity compared to the current model.

Finally, an open question is how different clients will **balance performance vs caution**. Some may use BAL more aggressively for parallel execution, while others may take a more conservative approach to reduce wasted work. The EIP leaves this choice open, allowing flexibility in implementation.

### Benefits vs Costs

Block-Level Access Lists (BAL) introduce a clear tradeoff: they bring meaningful performance improvements, but also add some overhead and complexity to the system.

On the benefits side, the biggest gain comes from **better use of hardware**. Since BAL provides a full view of which parts of the state will be accessed, clients can prefetch data from disk in advance and in parallel, reducing slow, repeated disk reads during execution. It also enables **parallel transaction execution** for independent transactions, allowing multiple CPU cores to be used effectively instead of processing everything sequentially. In addition, having a complete view of state changes makes it easier to optimize later steps like computing the final state root and even opens possibilities for faster syncing or reconstruction in the future. Overall, BAL shifts execution from a reactive process to a planned one, which leads to faster and more efficient block processing.

However, these benefits come with costs. First, there is **additional overhead** in tracking and storing all state accesses. Both the proposer and validator need to record detailed information about every account and storage slot touched during execution, which adds computation and memory overhead. Validators also need to rebuild the BAL during execution and compare it afterward, which is extra work compared to the current model.

There is also a block size impact. While the full BAL is not stored directly in the block body, it is transmitted alongside the block via the execution payload. According to the EIP’s analysis, the average BAL size is on the order of tens of kilobytes (around ~70 KB on average). This increases the amount of data that needs to be propagated across the network and processed by clients, though it is still considered manageable compared to existing block sizes. To keep this growth under control, the EIP introduces a constraint that limits the total number of items in the BAL. Specifically, the number of entries (addresses plus storage keys) must be bounded by the block gas limit (approximately `bal_items ≤ block_gas_limit / 2000`). This ensures that the BAL size scales with available gas and cannot grow arbitrarily large, keeping resource usage predictable and manageable.

Another cost is **increased validation complexity**. Clients must now handle more sophisticated logic: prefetching data, scheduling execution (possibly in parallel), tracking all accesses, constructing a virtual BAL, and verifying it against the proposer’s version. They also need to handle adversarial cases where the BAL is incorrect, which can lead to wasted work. This makes client implementations more complex compared to the current sequential model.

In simple terms, BAL trades simplicity for performance. It adds extra work and complexity to clients, but in return, it enables significant improvements in execution speed and resource utilization. The design ensures that correctness is never compromised, but achieving these gains requires more advanced handling inside the client.

### Future Impact of BAL

Block-Level Access Lists (BAL) can significantly change how Ethereum clients process blocks over time. By making state access known upfront, BAL enables **faster validation** in practice. Clients can load required data in advance, reduce disk stalls, and make better use of parallel hardware. Even though validators still execute transactions to verify correctness, the overall time spent per block can decrease because less time is wasted waiting on I/O and more work can happen concurrently.

BAL also supports **better scalability**. Today, adding more CPU cores does not help much because execution is largely sequential and often blocked by disk access. With BAL, independent work can be spread across multiple cores, and data can be fetched in parallel. As hardware improves (more cores, faster storage), clients can take greater advantage of these resources. In simple terms, BAL helps Ethereum scale with modern hardware instead of being limited by a single-threaded, disk-bound model.

Another important direction is **potential execution-less or reduced-execution sync**. Because BAL records not only what was accessed but also the resulting state changes, it provides a structured summary of how the state evolves across a block. In some contexts—especially for syncing—this opens the possibility of applying recorded state updates more directly instead of fully re-executing every transaction. While full execution is still required for strict validation, these recorded diffs can reduce the work needed in certain synchronization or reconstruction workflows.

Finally, BAL has a clear **impact on client design**. Clients will need to evolve from simple sequential executors into more advanced systems that can prefetch data, schedule work across multiple threads, track detailed access patterns, and verify results deterministically. This introduces more complexity, but it also aligns client architecture with modern systems design—where parallelism, batching, and data locality are key to performance. Over time, different clients may explore different strategies for using BAL efficiently, leading to further innovation in execution engines.