---
eip: 8268
title: Storage Roots in Block Access Lists
description: Extend EIP-7928 with per-account post-block storage trie roots so partially stateful nodes can verify the state root
author: Toni Wahrstätter (@nerolation), Carlos Perez (@CPerezz)
discussions-to: https://ethereum-magicians.org/t/eip-8268-storage-roots-in-block-access-lists/28585
status: Draft
type: Standards Track
category: Core
created: 2026-05-21
requires: 7928
---

## Abstract

This EIP extends [EIP-7928](./eip-7928.md) by attaching the post-block storage trie root to every Block Access List (BAL) entry that records state modifications. Partially stateful nodes can then verify the post-block state root without holding the full storage tries of modified accounts.

## Motivation

A BAL exposes per-slot post-values but not the post-block storage trie root of each modified account. A node that holds only a partition of the state therefore cannot derive the storage root for a modified account it does not store, and so cannot reconstruct the state-trie leaf needed to verify the post-block state root.

Including the post-block storage root for every modified account closes this gap. Combined with the balance, nonce, and code information already present in the BAL, any node can assemble the full state-trie leaf for every modified account regardless of which portion of state it retains.

## Specification

The key words "MUST", "MUST NOT", "SHOULD", and "MAY" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).

### Modified `AccountChanges`

Each `AccountChanges` entry whose state-change lists (`storage_changes`, `balance_changes`, `nonce_changes`, or `code_changes`) are not all empty MUST carry an additional trailing field `storage_root`:

```
StorageRoot = bytes32      # 32-byte trie root, or the empty byte string when the trie is empty

AccountChanges = [
    Address,
    List[SlotChanges],     # storage_changes
    List[StorageKey],      # storage_reads
    List[BalanceChange],   # balance_changes
    List[NonceChange],     # nonce_changes
    List[CodeChange],      # code_changes
    StorageRoot,           # post-block storage trie root (new)
]
```

`storage_root` is the root of the account's storage trie evaluated against the post-block state. For accounts whose post-block storage trie is empty, `storage_root` MUST be the empty byte string (RLP-encoded as `0x80`); the 32-byte canonical empty trie root `0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421` MUST NOT be used. A consumer MUST treat the empty byte string as that canonical empty trie root when reconstructing the state-trie leaf. This condition is keyed on the post-block storage trie being empty, not on account type, so externally owned accounts, newly created or cleared contracts, and [EIP-7702](./eip-7702.md) delegated accounts are all covered by the same rule without inspecting code.

Entries that describe accessed-but-unchanged accounts (every change list empty) MUST NOT include `storage_root` and retain the six-field layout from [EIP-7928](./eip-7928.md).

### Validation

A block is invalid if, for any `AccountChanges` carrying `storage_root`:

- the post-block storage trie is non-empty and `storage_root` is not its 32-byte storage trie root produced by executing the block; or
- the post-block storage trie is empty and `storage_root` is not the empty byte string.

Equivalently, every empty storage trie MUST be encoded as the empty byte string and every non-empty storage trie as its 32-byte root, so each block has exactly one valid encoding.

All other rules from [EIP-7928](./eip-7928.md), including ordering, uniqueness, `block_access_list_hash` computation, and engine API encoding, remain unchanged. `storage_root` is included in the RLP that the hash commits to.

## Rationale

The storage root is the single piece of post-block state that cannot be reconstructed from BAL contents alone, since it depends on the entire storage trie of the account rather than only the modified slots. Balance, nonce, and code are already recoverable from the BAL diffs, so committing to the storage root is sufficient to derive the full state-trie leaf. Restricting the field to accounts with state changes keeps the overhead at 32 bytes per modified account; touched-only entries add nothing.

Most accounts mutated in a block are externally owned accounts that change only balance or nonce and hold no storage, so their post-block storage trie is empty. Encoding that case as the 32-byte canonical empty root would repeat a well-known constant for the majority of entries. Representing the empty trie as the empty byte string (`0x80`) instead removes 32 bytes per such account while remaining unambiguous, since `0x80` cannot collide with any 32-byte root. Keying the rule on the post-block storage trie being empty, rather than on account type, keeps it correct for newly created or cleared contracts and for [EIP-7702](./eip-7702.md) delegated accounts, whose storage may be empty or non-empty, without inspecting account code.

## Backwards Compatibility

This proposal alters the BAL encoding introduced by [EIP-7928](./eip-7928.md) and requires a hard fork. The two encodings are not interchangeable: clients implementing only EIP-7928 will reject blocks under this EIP and vice versa.

## Security Considerations

`storage_root` is a redundant commitment over data already produced during execution, so any divergence is detected as part of normal BAL validation. The added bandwidth is bounded by `32 * modified_accounts`, well below the BAL size envelope described in [EIP-7928](./eip-7928.md).

Mandating a single canonical encoding for the empty storage trie — the empty byte string, never the 32-byte constant — ensures every block has exactly one valid BAL serialization. `block_access_list_hash` therefore stays deterministic, and clients cannot disagree on the commitment for identical post-block state.

## Copyright

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