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
- At the end of sync, missing Merkle trie nodes are requested one by one with GetTrieNodes (0x06) to resolve state inconsistencies.
- Because it is impossible to know in advance which nodes are still needed, searching and requesting repeat many times.
- Nodes exchange state over the snap/1 protocol.
After EIP-8189
- BALs of the blocks that passed during sync are fetched with GetBlockAccessLists (0x08), and the state changes are applied in block order.
- The list of blocks to catch up on is known in advance, so sync proceeds without iterative search, and each BAL is verified against the header hash.
- The protocol moves up to snap/2, and the GetTrieNodes and TrieNodes messages are removed.
Who is affected
- Client teams: Clients must implement snap/2 version negotiation, BAL storage and serving, and BAL-based catch-up and reorg recovery logic. With snap/2, trie node healing code is no longer needed.
- Node operators: Newly syncing nodes catch up on state by applying BALs instead of healing. Older clients can keep using snap/1.
- Serving node (peer): A node must respond to BAL requests, and a peer that often returns empty entries even though it holds the data may be given low priority by other nodes.
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.