EIP-8189 snap/2: BAL-based state healing (snap/2 - BAL-Based State Healing)

No call decision · Networking & nodes · EIP status Last Call · created 2026-03-06

Upgrades the snap sync protocol to snap/2 and replaces the healing phase, which fetched Merkle trie nodes one by one to reconcile state, with downloading and applying block access lists (BALs).

Why it is needed

Snap sync, which lets a new node catch up with the network, downloads the entire state as of a particular pivot block. However, the chain keeps advancing during the download, so the downloaded state diverges from the latest state. Currently, the healing phase that closes this gap requests the missing Merkle trie nodes one by one with GetTrieNodes messages. Which nodes are still needed is only known after receiving responses, so requests and responses must go back and forth many times. The block access list (BAL) introduced by EIP-7928 records every state change made in each block and puts its hash in the block header. A syncing node therefore only needs to fetch the BALs of the blocks that passed during the download and apply them in order. The set of blocks to catch up on is known in advance, so the iterative search disappears, and each BAL can be verified against the hash in the header. This EIP defines the exchange channel for that as version 2 of the snap protocol.

What changes

Today

After EIP-8189

Who is affected

In depth

snap/2 version and message set changes

The protocol version moves from snap/1 to snap/2, and peers negotiate the version in the RLPx capability handshake. GetTrieNodes (0x06) and TrieNodes (0x07), which were used for trie node healing, are removed, and GetBlockAccessLists (0x08) and BlockAccessLists (0x09) are added. The messages from 0x00 to 0x05 (GetAccountRange, AccountRange, GetStorageRanges, StorageRanges, GetByteCodes, ByteCodes) are identical to snap/1. Once BAL-based healing is in place, requests for trie nodes to repair state are no longer needed, so the existing messages are removed to avoid maintaining two healing mechanisms.

GetBlockAccessLists (0x08) request rules

The request format is [request-id: P, [blockhash₁: B_32, blockhash₂: B_32, ...], bytes: P]. bytes is the cumulative maximum size of BAL data the requester is willing to receive in one response, and the limit on the number of hashes per request is left to the implementation. A node must respond to a request, and if it has no BAL for a requested block it must return an RLP empty string (0x80) in that position. It may return fewer entries than requested because of the bytes soft limit or QoS limits, but in that case it must truncate from the end and keep the request order. BALs are served only for blocks after EIP-7928 activation and only within the retention period defined there. It is also recommended to keep the BALs of orphaned (non-canonical) blocks within the retention period. Because requests are keyed by block hash, orphaned BALs can be served in the same way, and having them is what allows recovery from a reorg across the pivot without restarting sync from scratch.

BlockAccessLists (0x09) response and validation

The response format is [request-id: P, [block-access-list₁, block-access-list₂, ...]], and each element corresponds to the requested block hash at the same position. An RLP empty string (0x80) is placed where a block has no BAL. The responding node should respect the bytes limit in the request, and the recommended soft limit when there is no separate limit is 2 MiB. A received BAL must be verified by computing keccak256(rlp.encode(bal)) and comparing it with the block-access-list-hash in the corresponding block header. The BAL encoding format follows EIP-7928.

Sync algorithm: BAL catch-up instead of healing

After downloading headers to find the chain head, pick a pivot block P sufficiently behind the head (for example, HEAD-64). It must be far enough back to reduce the chance that P is reorged, yet recent enough that serving peers still hold the state at that point in memory. The state as of P is bulk downloaded with GetAccountRange, GetStorageRanges and GetByteCodes. If the chain advances in the meantime and the pivot moves to P+K (K is usually 64), the BALs from P+1 to P+K are fetched, each is verified against its header hash, the state diffs are applied, and P+K becomes the new sync target. If peers stop holding the state as of P during the bulk download, the BALs up to a new pivot P' are applied in the same way and the download continues from P'. If the pivot advances again, the process repeats for the newly created blocks, and at the end the state root is recomputed and compared against the latest header.

Recovery path for a reorg across the pivot

If the chain reorgs behind the pivot block P, recovery works as follows, where W is the common ancestor of the old chain and the new canonical chain. Collect the BALs from W+1 to P on the old fork, find the state entries that changed on the old fork but not on the new fork, delete them locally, and fetch them again with GetAccountRange and GetStorageRanges. Then apply the BALs from W+1 on the new canonical chain and continue syncing with a new pivot. If the BALs of the orphaned blocks needed here cannot be obtained, the downloaded state must be discarded and sync restarted from scratch. This recovery path is also why peers are advised to retain orphaned BALs.

Why it is in snap rather than eth, and compatibility

Putting the BAL exchange in a separately versioned snap rather than in the eth protocol (for example, eth/71) follows the existing snap design philosophy. Sync primitives live in a satellite protocol, and eth is meant to remain a protocol dedicated to chain maintenance in the long run, for the same reason GetByteCodes was replicated from eth/65 into snap. snap is optional and versioned independently, so the sync feature is not tied to eth. On compatibility, a new version must be rolled out, and older clients keep using snap/1. snap/2 is meaningful only for post-Amsterdam blocks that have the block-access-list-hash header field. Even a node that supports both versions should use only one of them for state sync, and after finishing sync it can serve requests on both protocols.

Security considerations and status

There is an amplification risk, where one small request can trigger a much larger response, so implementations should rate-limit requests and respect the 2 MiB soft response limit. A peer that returns an empty entry for a block that should have data may be malicious or may have legitimately pruned it, so it is advisable to track peer reliability and lower the priority of low-reliability peers. BALs must be applied in block order and only after hash verification, and applying them out of order or applying a BAL from a different fork shows up as a wrong root in the final state root check. This EIP's status is Last Call, and its deadline (last-call-deadline) is 2026-11-01. On the core developer calls it was discussed at 08.03 ACDT #90 and 08.10 ACDT #91, and no decision such as fork inclusion has been made yet.

Original EIP · ← EIPs