---
eip: 8347
title: Offline State Migration to the PBT
description: Convert state to the PBT off the consensus-critical path, distribute a verifiable snapshot, and swap the commitment at a single fork
author: Carlos Perez (@CPerezz), Maria Silva (@misilva73), Kevaundray Wedderburn (@kevaundray)
discussions-to: https://ethereum-magicians.org/t/eip-8347-offline-state-migration-to-the-pbt/29089
status: Draft
type: Standards Track
category: Core
created: 2026-07-23
requires: 7928, 8159, 8297
---

## Abstract

This EIP specifies an **offline** migration of Ethereum's state from the hexary Merkle Patricia Trie (MPT) to the Partitioned Binary Tree (PBT) defined in [EIP-8297](./eip-8297.md).

The full state at a finalized **anchor block `ANCHOR_BLOCK`** is converted off the consensus-critical path and distributed as a byte-canonical, verifiable **PBT snapshot**. That snapshot is caught up to the chain tip by replaying Block-Level Access Lists (BALs) ([EIP-7928](./eip-7928.md)), and made canonical at a single EL+CL hard fork **`PBT_ACTIVATION_FORK`**, with both trees maintained through a transition window until that fork is finalized.

This proposal defines `ANCHOR_BLOCK`, `PBT_ACTIVATION_FORK`, the PBT snapshot artifact, the dual-check verification procedure, and the **shadow-root** observability concept.

## Motivation

Upgrading Ethereum's state trie, such as the move to the PBT in [EIP-8297](./eip-8297.md), means migrating live mainnet state to a new commitment without splitting the chain. There are two broad ways to do it. The **online overlay** family ([EIP-7748](./eip-7748.md), [EIP-7612](./eip-7612.md)) converts the state inside consensus while the chain keeps running. This EIP takes the **offline** path. It converts the state off the consensus-critical path and swaps the commitment at a single fork `PBT_ACTIVATION_FORK`.

Both paths can be made correct. The choice is a trade between two sets of costs. Going offline buys three things:

- **A smaller consensus change.** `PBT_ACTIVATION_FORK` changes the state commitment and nothing else: at one block, one header field starts being computed from a different tree. No conversion cursor lives in the state, and no MPT traversal order becomes consensus-critical.
- **No hybrid proof verifiers.** The canonical state root always commits to the entire state: the MPT root before `PBT_ACTIVATION_FORK`, the PBT root after. An overlay conversion commits only to the part already converted, so proving a key still held in the base MPT needs a second proof against the frozen MPT root. That cost falls on light clients, on-chain bridge contracts, and consumers of state proofs.
- **No in-consensus conversion for zkEVM provers.** Provers prove plain execution before the swap and commit to the PBT root after. They never prove a conversion step.

In exchange it pays two costs:

- **More disk.** A node holds both the MPT and the PBT through the transition window, roughly doubling state storage for that period.
- **No on-chain observability of the conversion.** Conversion happens off-chain, so consensus does not see whether it was done correctly. This EIP recovers that visibility off consensus, through the [shadow-commitment period](#shadow-commitment).

The classic downsides of going offline are distribution trust and catch-up correctness. Both are removed by the [dual-check verification](#verification-dual-check) and [BAL-replay](#bal-replay) defined below. The trade is worked through in [Why offline conversion instead of the online overlay](#why-offline-conversion-instead-of-the-online-overlay).

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" 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).

### Parameters

| Symbol | Meaning | Value |
| --- | --- | --- |
| `ANCHOR_BLOCK` | Anchor block whose state is converted; MUST be finalized and is identified by block hash | TBD, selected during the mainnet window (post-rehearsal) |
| `PBT_ACTIVATION_FORK` | The EL+CL hard fork at which the PBT becomes the canonical state commitment. This EIP names it but does not schedule it | Set by the hard fork meta EIP that includes this proposal, after the shadow-commitment period |
| `REANCHOR_CADENCE` | Re-anchoring cadence for late joiners, in blocks | 50400 (~1 week at 12s slots) |

### Process overview

The sections below define the pieces of the migration: the preimages, the converter, the PBT snapshot artifact, the dual-check verification, BAL-replay, and the shadow commitment. This section shows how they fit into one lifecycle, which carries the network from an MPT-committed state to a PBT-committed state without conversion ever touching the consensus-critical path. Consensus runs on the MPT through every pre-swap phase; the commitment does not change until `PBT_ACTIVATION_FORK`.

The migration proceeds in five phases:

1. **Conversion (off-chain).** A finalized block is designated as `ANCHOR_BLOCK`. Conversion produces two artifacts in two steps:
   1. **Extract the [preimages](#preimages).** The MPT is hash-keyed, so re-deriving its paths needs the raw account addresses and storage-slot keys. Keccak cannot be inverted, so the set comes from a client's retained preimages, from [EIP-7928](./eip-7928.md) BALs, or from chain replay, never from the hashed paths themselves.
   2. **Generate the [PBT snapshot](#pbt-snapshot-artifact).** The [converter](#the-converter) reads the state committed by `ANCHOR_BLOCK`'s `stateRoot`, together with the preimages, derives PBT keys per [EIP-8297](./eip-8297.md), and produces the PBT snapshot: a byte-canonical artifact holding the converted PBT.

   Both steps are pure functions of the state committed by `ANCHOR_BLOCK`'s `stateRoot`, so independent producers MUST emit bit-identical output, and both happen entirely off consensus.

2. **Distribution and verification.** The PBT snapshot and preimages are published off-chain. Any node obtains them, either by downloading or by self-converting, and MUST run the [dual-check verification](#verification-dual-check): rebuild the PBT and confirm the claimed root (internal consistency), then re-hash the leaves under the MPT schema, using the preimages, and confirm they reproduce `ANCHOR_BLOCK`'s `stateRoot` (consensus anchoring). This lets a node reject a corrupted or malicious artifact without trusting the distributor, and no release-anchored value is required.

3. **Catch-up.** `ANCHOR_BLOCK` lags the chain tip, and keeps lagging as new blocks are produced. A node brings its verified PBT snapshot forward by [BAL-replay](#bal-replay): applying the recorded writes of each subsequent block's Block-Level Access List ([EIP-7928](./eip-7928.md)) directly to the PBT, with no re-execution. BALs sit outside the block body and are fetched from peers over `eth/71` ([EIP-8159](./eip-8159.md)). Replay MUST outpace block production so the converted state converges to and then tracks the tip, and from here the node maintains the PBT alongside its MPT. To bound the replay gap for nodes joining late, distributors [re-anchor](#re-anchoring) on a fixed cadence, which also keeps the range a node has to replay inside the [EIP-7928](./eip-7928.md) retention window (see [BAL availability](#bal-availability)).

4. **Shadow-commitment period.** Attesters SHOULD publish a [shadow root](#shadow-commitment): the PBT root of each block's post-state, signed with their validator key. Phases 2 and 3 are per-node onboarding, run on each node's own schedule, and a node starts publishing as soon as its own PBT tracks the tip, so this phase overlaps the others rather than following a network-wide switch. Consensus still runs on the MPT throughout, so the root carries no weight yet; divergence is visible and harmless, and bugs are caught and fixed ahead of the swap rather than after. The length of this period is what builds confidence that the PBT root is correct across clients.

5. **Swap and transition window.** At the coordinated EL+CL hard fork `PBT_ACTIVATION_FORK`, the PBT becomes the canonical state commitment. Nothing else about execution changes. The [transition window](#transition-window) runs from `PBT_ACTIVATION_FORK` activation until it is finalized. Through this window, nodes MUST maintain both trees and MUST keep the migration recoverable to the MPT. Only after finality MAY a node dispose of the MPT.

#### Actors and roles

Conversion and distribution are **permissionless and untrusted**. Because the conversion output is a pure function of the state committed by `ANCHOR_BLOCK`'s `stateRoot`, and because the [dual-check verification](#verification-dual-check) lets any node reject a bad artifact without trusting its source, the identity of whoever produces or serves the PBT snapshot is irrelevant to correctness. Three roles are involved:

- **Producers** run the [converter](#the-converter) to generate the PBT snapshot and preimages. Any party MAY be a producer: client teams, infrastructure providers, or a node self-converting from its own MPT. Self-conversion is only available to a node that still holds the MPT state at `ANCHOR_BLOCK`; a node that has pruned it (i.e. `ANCHOR_BLOCK` falls outside its retained-state window) MUST instead obtain the artifacts by download and run the [dual-check](#verification-dual-check). All correct producers emit bit-identical artifacts for a given anchor.
- **Distributors** serve the artifacts off-chain over existing infrastructure (snap-sync-style peer-to-peer, era-style files, Content Delivery Networks (CDNs), or torrents). Any party MAY be a distributor; these transports already provide partial-download integrity and resumption, and the [dual-check](#verification-dual-check) makes the full artifact safe to accept from an untrusted distributor.
- **Consumers** are **every node**, validators included. A consumer MUST obtain the artifacts (by download or self-conversion), run the dual-check, and catch up to the tip by [BAL-replay](#bal-replay). Validators take on one extra duty, and it is observability-only: attesters SHOULD publish [shadow roots](#shadow-commitment), signed with their validator key.

No protocol rule requires validators to produce or serve the conversion. In practice, the network SHOULD rely on at least one reliable, well-known distribution source, so that nodes joining around `PBT_ACTIVATION_FORK` do not depend on chance availability. That source is expected to run conversion and serving at every [re-anchor point](#re-anchoring) through the [shadow-commitment period](#shadow-commitment). This is an operational commitment, not a consensus requirement.

### Preimages

The MPT is hash-keyed, and most clients do not persist the plain account addresses and storage-slot keys behind the trie paths, so those keys cannot in general be recovered from a client's own database. The PBT is hash-keyed too: [EIP-8297](./eip-8297.md) keys an account's header stem by `key_hash(address)`, and an overflow storage group by the pair `key_hash(address) || key_hash(address || tree_index)`. A PBT leaf carries no preimage of its own, so holding the PBT does not give a node the keys behind it. Key preimages MUST therefore be distributed alongside the PBT snapshot. They serve two consumers: self-converters running on hash-keyed clients, and verifiers running the consensus-anchoring check, which re-hashes the leaves under the MPT schema and cannot run without them. Preimages MUST be extracted at `ANCHOR_BLOCK`, so the set corresponds exactly to the state committed by `ANCHOR_BLOCK`'s `stateRoot`.

Recovering the full set is the expensive part, and the anchor state alone does not yield it, since Keccak cannot be inverted. A preimage is either retained when the key is first seen, or recovered by replaying the chain. Block-Level Access Lists ([EIP-7928](./eip-7928.md)) carry plain addresses and slot keys, so they yield preimages for every key touched since BAL activation, but the anchor state also holds long-dormant keys untouched since then. A complete set therefore needs a client-side preimage store or a full replay; BALs shorten the work without removing it.

The preimage file is a concatenation of per-account records with no framing between them. Each record is the RLP encoding of:

```text
[address, [slotKey, slotKey, ...]]
```

that is, the 20-byte account address followed by the list of that account's storage-slot keys. An account with no storage carries an empty list. RLP framing makes each record self-delimiting, so the file is parsed by decoding records sequentially until end of input. The file ends immediately after the final record. There is no terminator, padding, or trailing newline, and any trailing byte makes the file invalid.

The file MUST be byte-canonical, so independent producers emit bit-identical output. To achieve this, the encoding and ordering are fixed:

- `address` MUST be encoded as exactly 20 bytes, including any leading zero bytes.
- Each `slotKey` MUST be encoded as a canonical RLP integer: the 256-bit slot number in big-endian form, with all leading zero bytes removed. Slot 0 encodes as the empty string, slot 1 as a single byte. A leading zero byte makes the record invalid. Most slot keys are mapping-derived and fill all 32 bytes, but low-numbered slots are common and this drops their padding.
- Records MUST be sorted by `address` ascending, compared as 20-byte big-endian (byte-lexicographic) values. Each address MUST appear at most once.
- Within a record, `slotKey` entries MUST be sorted ascending by slot number, with no duplicates.

A verifier recovers the MPT account path as `keccak256(address)` and each storage path as `keccak256(slotKey)`, where `slotKey` is first left-padded back to 32 bytes. This is what makes the consensus-anchoring check against `ANCHOR_BLOCK`'s header `stateRoot` possible.

Grouping slot keys under their account is deliberate. A globally deduplicated list would be smaller, but only slightly: the repetition sits in low-numbered slots, while most storage keys are mapping-derived and unique to one account. Grouping buys something better, namely that a converter can stream one account's storage trie at a time rather than hold a global lookup table in memory.

### The converter

The converter is a deterministic function from MPT state to PBT state. Given the state at `ANCHOR_BLOCK` (i.e., the MPT, the contract bytecode it references, and the preimages), it MUST:

1. Scan the source (MPT) leaves, recovering each leaf's full hashed path. A leaf node holds only the tail of that path, so the walk accumulates the rest from the branch indices and extension segments above it. The path is `keccak256(address)` in the account trie and `keccak256(slotKey)` in a storage trie, where `slotKey` is the 32-byte big-endian slot number.
2. Index the preimages by `keccak256(preimage)`, and require that this index and the set of leaf paths from step 1 match **exactly**, in both directions. A leaf path with no preimage means the set is incomplete. A preimage matching no leaf path means the set is not the one committed by `ANCHOR_BLOCK`'s `stateRoot`. The converter MUST reject either case. Per-leaf hash equality is not a check on its own, since a preimage is only ever found by hashing it. What is enforced is the exact match of the two sets.
3. Derive PBT keys per [EIP-8297](./eip-8297.md) using the preimages. Most of an account's state lands under its header stem in `ACCOUNT_ZONE`: basic data, `code_hash`, storage slots 0 through 63 at sub-indices `HEADER_STORAGE_OFFSET`..`127`, and code chunks 0 through 127 at sub-indices `CODE_OFFSET`..`255`. Only slots 64 and above take storage-zone keys.
4. For each account with code, fetch the bytecode from the code store using the account's `code_hash`, chunk it per [EIP-8297](./eip-8297.md), and emit the resulting code leaves. Chunks 0 through 127 are header-stem leaves of that account, as in step 3. Chunks 128 and above are overflow chunks in `CODE_ZONE`, content-addressed by `code_hash`, so accounts sharing bytecode share them: a converter MUST emit each overflow leaf exactly once, since a duplicate key would break byte-canonicality.
5. Sort leaves by PBT key order (an external merge-sort for mainnet-scale state).
6. Construct the tree bottom-up in a single sequential pass.

Independent, correct converters operating on the same state at `ANCHOR_BLOCK` MUST produce a bit-identical PBT root and a bit-identical canonical PBT snapshot. Sorting in PBT-key order lets PBT snapshot ingestion be a sequential bulk-load rather than random inserts.

Step 2 is not redundant with the [dual-check](#verification-dual-check). A missing preimage does surface there, as a missing PBT leaf and so a root mismatch, but only at the end of a full rebuild and without naming the key at fault; step 2 names it in one pass. A surplus preimage does not surface there at all: consensus anchoring rebuilds the MPT from the PBT leaves, so an entry matching no leaf is never read and the roots still agree. Step 2 is what holds the preimage file to the exact set committed by `ANCHOR_BLOCK`'s `stateRoot`, and so what makes it byte-canonical across producers.

A self-converter's input MPT state is its own, whose correctness the node already established by executing the chain to `ANCHOR_BLOCK` and checking its computed state root against the header `stateRoot`. That input needs no separate provenance check; still holding the state at `ANCHOR_BLOCK` (see [Actors and roles](#actors-and-roles)) is the only additional requirement.

### PBT snapshot artifact

The conversion result is published as a PBT snapshot with the following properties:

- **Byte-canonical.** Serialization is fully specified below, so independent producers MUST emit bit-identical output.
- **Sorted in PBT-key order** to enable bulk ingestion.

The artifact is a small fixed header followed by the sorted leaf-record stream:

```text
pbtRoot[32] | leafCount[8, big-endian] | leafRecord * leafCount
```

`pbtRoot` is the claimed PBT root, `leafCount` the number of leaf records, and each `leafRecord` is encoded as defined under [Leaf record](#leaf-record) below. Both header fields are recomputed and checked by the [dual-check](#verification-dual-check).

The snapshot contains **only the PBT leaves**; it carries no intermediate (inner) tree nodes. Every node reconstructs those locally by hashing the leaves bottom-up during [verification](#verification-dual-check). This keeps the artifact minimal and forces each node to derive the tree itself rather than trust distributed inner hashes: the only inner value in the artifact is the claimed `pbtRoot`, which is recomputed from scratch and checked, never trusted.

The root of trust for the artifact is `ANCHOR_BLOCK`'s `stateRoot`, which a node obtains from its own header chain. A client release MAY additionally pin the `pbtRoot` of the designated swap anchor as a cross-check, but this is optional hardening and cannot cover the intermediate [re-anchor](#re-anchoring) snapshots, which are produced after the release ships.

How the artifact is split for transport is left to the distribution layer. The transports in use (snap-sync-style peer-to-peer, era-style files, CDNs, torrents) already provide partial-download integrity, parallelism, and resumption, and a distributor MAY additionally publish a chunk-hash index as a non-normative download accelerator. A distributor MAY also publish the tree's inner nodes down to a fixed depth, as a second accelerator: the snapshot is sorted in PBT-key order, so a node holding them can hash each completed subtree as it arrives and check it against the published value, catching a peer serving bad leaves during the download rather than at the end of it. Neither accelerator is ever trusted, and neither affects correctness, because the [dual-check](#verification-dual-check) re-derives the tree from the leaves and authenticates the reassembled artifact against `ANCHOR_BLOCK`'s `stateRoot`.

#### Leaf record

Each leaf of the [EIP-8297](./eip-8297.md) PBT is serialized as the RLP encoding of:

```text
[key, value]
```

where `key` is the full PBT tree key and `value` its 32-byte leaf value. As in the [preimage file](#preimages), RLP framing makes each record self-delimiting and the encoding is pinned so that producers agree byte for byte:

- `key` MUST be encoded at its full zone-determined length, including any leading zero bytes.
- `value` MUST be encoded as a canonical RLP integer: the 32-byte leaf value in big-endian form, with all leading zero bytes removed. A zero value encodes as the empty string. A leading zero byte makes the record invalid. This is where most of the saving is. [EIP-8297](./eip-8297.md) packs `version`, `code_size`, `nonce`, and `balance` into the basic-data leaf, low-magnitude fields first. So a typical account header value carries a long run of leading zero bytes, as do small storage values.

A verifier left-pads `value` back to 32 bytes before hashing it into the tree; leaf hashing is defined over the full 32-byte value, unchanged from [EIP-8297](./eip-8297.md).

The leading **zone byte** of `key` fixes its length, so a decoder knows what it is reading and MUST reject a key whose length disagrees with its zone:

| Zone byte | Zone | Key length, zone byte included |
| --- | --- | --- |
| `0x00` | account header (basic data, `code_hash`, storage slots 0-63, code chunks 0-127) | 34 |
| `0x01` | code, content-addressed overflow chunks only | 34 |
| `0xFF` | storage, slots 64 and above only | 66 |

An account or code key is `zone[1] | stem[32] | subindex[1]`, and a storage key is `zone[1] | stem[64] | subindex[1]`, per [EIP-8297](./eip-8297.md). A decoder MUST also reject any key in the `0x02`-`0xFE` range, which [EIP-8297](./eip-8297.md) reserves and no anchor-state leaf can occupy.

The trailing byte of every key is the **sub-index**. Two records belong to the same **stem** if and only if their keys are equal in all bytes except the sub-index. In other words, `key[:-1]` is identical. Because the PBT snapshot is sorted in PBT-key order, all records sharing a stem are contiguous.

#### Canonical digests

Both distributed files are byte-canonical, so each has a single digest that every correct producer reproduces for a given anchor:

- `snapshotDigest` is `keccak256` over the entire PBT snapshot byte stream, header included.
- `preimageDigest` is `keccak256` over the entire [preimage file](#preimages).

Neither is a root of trust; a node runs the [dual-check](#verification-dual-check) whatever the digests say and however the bytes arrived. What the digests buy is cheap agreement ahead of an expensive download. Each is 32 bytes, so a node MAY compare them across several independent producers or distributors and decline a source that disagrees, rather than discovering the disagreement after reassembling and rebuilding the whole snapshot.

`pbtRoot` cannot serve this purpose. It commits to the leaf set rather than to the byte stream, and it can only be recomputed once every leaf is present, so it says nothing about a partial download. For the same reason it does not authenticate the optional chunk-hash index or the optional inner nodes above. A distributor publishing either SHOULD also publish a digest over it, so that it can be cross-checked the same way before it is relied on to localize a fault mid-download.

### Verification (dual-check)

Any node MUST be able to verify a downloaded PBT snapshot without trusting the distribution source. This includes a fresh node with no prior state. Because the snapshot carries only leaves and no intermediate nodes, verification is also the step in which each node **derives the full tree for itself**. To verify, the node performs both checks:

1. **Internal PBT consistency.** Rebuild the PBT from the PBT snapshot leaves: derive keys per [EIP-8297](./eip-8297.md), compute every intermediate node by hashing bottom-up, and verify that the resulting root matches the claimed PBT root. The intermediate nodes are a product of this step, not part of the distributed artifact.
2. **Consensus anchoring.** Re-hash the PBT snapshot leaves under the **MPT schema** using the distributed preimages and verify the result against block `ANCHOR_BLOCK`'s header `stateRoot`. That covers every field the MPT commits: nonce, balance, storage, and `code_hash`.

   The MPT holds no code, so the re-hash alone leaves the code leaves and `code_size` unchecked. They are anchored instead to the `code_hash` it does cover. For each distinct `code_hash`, reassemble the bytecode from the 31-byte slices of that code's chunk leaves in chunk order, truncate the result to `code_size`, and require its `keccak256` to equal that `code_hash`. Then re-chunk the recovered bytecode per [EIP-8297](./eip-8297.md) and require the resulting leaves to equal the artifact's code leaves byte for byte, which pins each chunk's leading PUSHDATA count as well as its contents and its placement across the header stem and `CODE_ZONE`. A wrong `code_size` yields different bytes, so the same preimage pins it. `version` is the one remaining field the MPT does not commit; [EIP-8297](./eip-8297.md) sets it to zero on every header write, so every basic-data leaf MUST carry `version == 0`.

A PBT snapshot that fails either check MUST be rejected, and the node MUST re-obtain or re-convert.

Without the code limb of check 2, a snapshot carrying corrupted or wholly absent code leaves would pass both checks: internal consistency recomputes the root over whatever leaves it is given, and the MPT re-hash never reads them. A downloading node has no other source of bytecode, since only a self-converter holds a code store.

### BAL-replay

State transition from the anchor to the tip is performed by replaying Block-Level Access Lists ([EIP-7928](./eip-7928.md)) **without re-execution**. For each block after `ANCHOR_BLOCK`, per-entry translation rules apply the recorded writes to the PBT:

- Balance and nonce changes are applied to the account header's basic-data leaf, and storage writes are applied as PBT leaf writes. Code changes are applied as described under [replay deletion semantics](#replay-deletion-semantics).
- Accounts are almost never deleted, and replay needs no account-deletion marker in the BAL. An account's nonce only ever increases and cannot be cleared, and every path that would zero its balance bumps that nonce, [EIP-7702](./eip-7702.md) delegation included, so a funded account does not silently vanish. [EIP-6780](./eip-6780.md) leaves one path open: it confines `SELFDESTRUCT` to the transaction that created the account, and an address that already holds a balance is still *created* by `CREATE`/`CREATE2`. So a balance-only account present at `ANCHOR_BLOCK` can be deployed and destroyed in one later transaction, and is then gone from the MPT. Replay reaches that case from the account's own post-state rather than from a BAL marker, under [emptiness](#replay-deletion-semantics) below.
- Replay SHOULD be batched over a range of blocks, coalescing repeated writes to the same key so that each key is written once with its final value. Only the end state of the replayed range is needed, so writing intermediate values is wasted work. Batching MUST keep the replay rate above steady-state block production, so that a converted PBT snapshot converges to the tip and then tracks it.

#### Replay deletion semantics

Replay MUST preserve one invariant: the PBT obtained by replaying from `ANCHOR_BLOCK` to a block `B` MUST be bit-identical to the PBT obtained by converting the MPT state at `B` directly. Both routes are in use at once. [Re-anchoring](#re-anchoring) publishes freshly converted snapshots at intermediate blocks, and the [shadow commitment](#shadow-commitment) compares roots reported by nodes that reached the same block by different routes. Divergence between the two would look like a conversion bug when it is not one.

The MPT carries no entry for a zero-valued storage slot, no code at all, and no empty account, so holding that invariant forces replay to delete leaves that a direct conversion would never have produced. Deletion is therefore **migration-only**: it applies from `ANCHOR_BLOCK` until `PBT_ACTIVATION_FORK`, and it governs replay, never execution.

- **Storage.** A replayed storage write of zero MUST NOT insert a leaf: if a leaf for that key exists it MUST be deleted, and if none exists the write is a no-op. This holds wherever the slot lives, in the account's header stem for slots 0 through 63 or in the storage zone above that; deleting a header-stem storage leaf never removes the stem, which always retains its basic-data and `code_hash` leaves. [EIP-7928](./eip-7928.md) records the zeroing of a live slot as a write, and an unchanged write to an already-zero slot as a read. So only the first case arises in practice. The second is stated for completeness.
- **Code.** A code change MUST recompute the chunk leaves for the new code. Deletion is scoped to the **header** chunk leaves, which [EIP-8297](./eip-8297.md) holds in the account's own stem at sub-indices `CODE_OFFSET`..`255`, covering code chunks 0 through 127: replay MUST delete every header chunk leaf of the previous code that the new code does not cover, since code that shrinks otherwise leaves stale trailing chunks behind. Overflow chunks MUST NOT be deleted. They live in `CODE_ZONE`, content-addressed by `code_hash`, so contracts with identical bytecode share them, and removing them would strip code from an account that still runs it. Nor does replay ever need to: deployed code is immutable, since [EIP-6780](./eip-6780.md) confines `SELFDESTRUCT` to the transaction that created the account, and a delegation indicator is 23 bytes, so it occupies chunk 0 and never overflows. Every code change replay sees is therefore either a creation, which only adds leaves, or a delegation write confined to the header stem. Clearing an [EIP-7702](./eip-7702.md) delegation is the sharpest case: code returns to empty, so the header chunk leaf at sub-index `CODE_OFFSET` holding the indicator MUST be deleted, while the `code_hash` and basic-data leaves are updated rather than deleted, since a codeless account still commits to the Keccak hash of empty bytecode and to a `code_size` of zero.
- **Emptiness.** If, after a block's writes are applied, an account's basic-data leaf would hold `nonce == 0`, `balance == 0`, and `code_size == 0`, the account is empty, and [EIP-7523](./eip-7523.md) leaves no empty account in the MPT. A direct conversion at that block therefore produces no leaves for it, so replay MUST delete its header stem together with any storage-zone leaves in its bucket, which the zone layout groups under the single shared prefix `STORAGE_ZONE || key_hash(address)`. Its `CODE_ZONE` overflow chunks are content-addressed and MUST NOT be deleted, as above. This is the only case in which replay removes an account, and the [EIP-6780](./eip-6780.md) path above is what reaches it: [EIP-7928](./eip-7928.md) records the destroyed account's balance change to zero but no nonce or code change, and records the storage it touched as reads, so replay never inserted storage for it in the first place. The trigger is exact in both directions, since an empty account is never present in the MPT and a non-empty one is never absent from it.
- **Canonicalization.** A `BranchNode` in [EIP-8297](./eip-8297.md) always has two children, so deleting a leaf can leave a branch holding one. Replay MUST then merge the surviving sibling into that branch, the exact inverse of the split [EIP-8297](./eip-8297.md) performs on insertion: a leaf sibling replaces the branch outright, and a branch sibling with prefix `q` replaces it with prefix `p || b || q`, where `p` is the removed branch's prefix and `b` is the bit that selected the sibling. Deleting the last remaining leaf leaves the empty tree, whose hash is `[0x00] * 32`. Without this step, two nodes holding the same key/value set would commit to different roots.

At `PBT_ACTIVATION_FORK` these semantics end. From activation, [EIP-8297](./eip-8297.md) governs: writing zero stores a zero-valued leaf, distinct from an absent key, and execution never removes entries from the tree. Nothing is rewritten at the fork; the activated tree is whatever replay produced for the last pre-activation block. So when [EIP-8297](./eip-8297.md) says implementations never need delete logic, that holds for execution: the delete-and-merge path above is required by migration replay alone.

#### Reorg handling

Rollback MUST be performed by discarding state, never by reversing writes. [EIP-7928](./eip-7928.md) records post-values only (`new_value`, `post_balance`, `new_nonce`), so a BAL cannot be inverted: undoing a replayed block would require the values it overwrote, and they do not appear in the BAL.

- **While catching up.** The node's PBT head lags the tip. If a reorg's common ancestor is at or above that head, no replayed block is affected and replay simply continues along the new canonical chain when it reaches the reorganized range.
- **While tracking the tip.** A node MUST be able to roll its PBT back to any block within the unfinalized window. It SHOULD do so by holding the PBT of the most recent finalized block as a durable base and treating each block after it as a discardable layer, discarding layers back to the common ancestor on a reorg and replaying the new branch from there. The work is bounded by the reorg depth, which cannot exceed the distance to finality, and replay performs no execution, so recovering that distance is cheap. This is the discipline a client already applies to unfinalized MPT state, so no migration-specific rollback machinery is required.
- **Across an anchor.** `ANCHOR_BLOCK` and every [re-anchor point](#re-anchoring) MUST be finalized before their artifacts are produced, so a reorg across an anchor requires a finality violation and sits outside normal operation. Were one to occur, every snapshot anchored at or after the reverted block is invalid, and the migration MUST be re-anchored to a block on the new canonical chain.
- **Across the swap.** A reorg spanning `PBT_ACTIVATION_FORK` rewinds to a pre-activation block, where the canonical commitment is the MPT root again. The PBT for the new pre-activation branch is produced by ordinary replay, and the [transition window](#transition-window) requires both trees to be maintained precisely so that this stays possible.

Shadow roots MUST be keyed by block hash rather than by height. Two nodes reporting for the same height on opposite sides of a reorg hold different states and correctly report different roots. Keying by height would misread that as divergence.

#### BAL availability

BALs are not part of the block body, so a node cannot recover them from block data it already holds. A node obtains the BALs it needs for replay from peers over the `eth/71` protocol defined in [EIP-8159](./eip-8159.md), using `GetBlockAccessLists` / `BlockAccessLists`. That protocol is the transport this migration relies on; no migration-specific wire protocol or gossip is introduced.

Replay therefore depends on BALs being available for the range a node actually has to cover, and [EIP-8159](./eip-8159.md) lets a peer answer with an empty entry for a block whose BAL it has pruned. [Re-anchoring](#re-anchoring) bounds that range: a joining node starts from the most recent published anchor, never an older one, so the required range is at most `REANCHOR_CADENCE` plus the lag between a re-anchor point and the publication of its artifacts (the point must be finalized first, and conversion takes time).

`REANCHOR_CADENCE` MUST be chosen so that this bound stays inside the [EIP-7928](./eip-7928.md) retention window, which is the weak subjectivity period (3533 epochs, roughly 113000 blocks). At the value specified here, 50400 blocks (~1 week), two full cadences fit inside that window. Ordinary [EIP-7928](./eip-7928.md) retention thus already keeps every BAL a node could need available over `eth/71`, and this EIP imposes no retention requirement of its own.

### Re-anchoring

The BAL-replay gap between an anchor and the tip grows without bound as the chain advances, so a node that begins catch-up long after conversion would have to replay the entire elapsed chain. To bound that gap, a distributor re-runs the conversion at successive finalized anchors on a fixed cadence and publishes a fresh snapshot and preimage set for each. Re-anchor points are the blocks whose height is that of `ANCHOR_BLOCK` plus `n * REANCHOR_CADENCE` for `n = 1, 2, ...`, and each MUST be finalized before its artifacts are produced. The cadence also bounds how many BALs a node needs, which is what keeps replay inside the [EIP-7928](./eip-7928.md) retention window (see [BAL availability](#bal-availability)).

Each re-anchored snapshot is a self-contained artifact anchored to its own block's `stateRoot`, verified by the same [dual-check](#verification-dual-check), with no dependency on earlier anchors. A joining node selects the most recent re-anchor at or below finality, verifies it, and BAL-replays only from that anchor forward. Through the [shadow-commitment period](#shadow-commitment) the reliable distribution source SHOULD run conversion and serving at every re-anchor point, so that a node joining at any time has a recent snapshot to start from.

### Shadow commitment

Through the pre-swap period, consensus still runs on the MPT. In that period attesters SHOULD compute the **shadow root** of each block, the PBT root of that block's post-state, and publish it signed with their validator key. Conversion correctness then becomes observable per block, in public, and traceable to whoever reported it.

Shadow roots travel in an **out-of-consensus telemetry sidecar**, and publication is never a block-validity condition. A missing or late shadow root counts against a **coverage** metric rather than as a divergence, which is what keeps the signal readable. The sidecar is expected to ship enabled by default in consensus-layer clients, so coverage comes from ordinary validator operation rather than from an opt-in program; like the reliable distribution source, that is an operational expectation, not a consensus requirement.

Wire format, aggregation scheme, publication timing, and any EL-to-CL plumbing are out of scope here. They are left to a companion EIP, planned but not yet published; nothing about them is settled elsewhere, and this EIP assumes nothing about them.

### Activation

At `PBT_ACTIVATION_FORK` the PBT becomes the canonical state commitment. The change is confined to which tree produces the value in the existing `stateRoot` header field.

- **No payload-structure change.** No header field is added, removed, reordered, or retyped. `stateRoot` keeps its name, position, and 32-byte type; before activation it holds the MPT root, and from activation the PBT root computed per [EIP-8297](./eip-8297.md).
- **The boundary is a block, not a range.** The activation block is the first whose post-state is committed as a PBT root, and its parent's `stateRoot` is an MPT root. No block commits to both, and nothing before activation is rewritten.
- **The consensus layer treats the root opaquely.** `ExecutionPayload.state_root` is 32 opaque bytes to the beacon chain, which never interprets or recomputes it, so the consensus-layer change is fork scheduling alone. No new payload field is introduced either, so this EIP requires no engine-API method or version change; whether the fork mints a new version for other reasons is left to the meta EIP that schedules it.
- **Shadow roots end here.** From activation the PBT root is the canonical `stateRoot`, validated by consensus on every block, so the sidecar has nothing left to observe. Attesters SHOULD stop publishing shadow roots at activation, and clients MAY remove the sidecar in a later release.

### Transition window

Both the MPT and the PBT MUST be maintained from the activation of the PBT at `PBT_ACTIVATION_FORK` until `PBT_ACTIVATION_FORK` is finalized. Only after finality MAY a node dispose of the MPT. Until the activation release deploys, the migration MUST remain fully recoverable to the MPT.

## Rationale

### Why offline conversion instead of the online overlay

[EIP-7748](./eip-7748.md) and [EIP-7612](./eip-7612.md) convert the state inside consensus, behind a boundary that moves forward one block at a time. Both that online overlay and this offline design can be made correct, so the choice is about which costs to pay. This EIP takes the offline path. It pays more disk and gives up on-chain observability of the conversion, and in return gets a smaller consensus change, whole-state proofs throughout, and no conversion inside any zkEVM proof. Each of the five points is worked through below.

**A smaller consensus change.** An overlay's read rule is simple: try the overlay tree, fall back to the base MPT. The cost is the cursor that drives the conversion to completion, because it lives in the state. [EIP-7748](./eip-7748.md) tracks which account is being converted, how far into it the conversion has got, and whether it has finished, advancing by `CONVERSION_STRIDE` units every block. So every client must agree on the cursor bit for bit at every block, the order of MPT traversal becomes consensus-critical, avoiding stale writes over fresher values becomes a correctness condition that surfaces only as a wrong state root, and the stride has to be chosen for long reorgs, which rewind the cursor with the rest of the state. `PBT_ACTIVATION_FORK` adds none of that: no conversion state, no per-block schedule, no ordering requirement, just one header field computed from a different tree at one block.

**No hybrid proof verifiers.** Either way a new tree changes proof formats, and either way the header carries exactly one root, since [EIP-7612](./eip-7612.md) also swaps the header state root at its fork. The difference is how much that root covers. Offline, it covers the whole state at every block. During an overlay conversion it covers only the part converted so far, so proving a key still held in the base MPT takes a second proof against the frozen MPT root, which no header carries and which the verifier must pin out of band; proving absence takes both. So an on-chain bridge verifier needs three deployments under an overlay against two switching at a known block: an MPT verifier, a hybrid verifier carrying an out-of-band root, then a PBT verifier. The middle one is the expensive one, the only one that is not a plain tree lookup, built for a bounded window and then maintained anyway.

**No in-consensus conversion for zkEVM provers.** Block validity proofs are expected to be live by swap time. In an overlay, a prover must prove the conversion steps for every block in the window on top of proving normal execution. Offline conversion never enters a proof: provers keep proving plain MPT execution until `PBT_ACTIVATION_FORK`, then commit to the PBT root. How large the avoided cost is depends on how heavy a conversion step is to prove and on how mature zkEVM proving is by then, but the point is structural: only the online design pays it at all.

**More disk.** This is the main cost of going offline. An overlay can delete converted entries from the old tree as it goes, so it stores about one tree throughout. Offline holds both through the transition window, an extra ~300 GB at mainnet scale. That cost is temporary and confined to converting nodes, and holding the MPT is what keeps the migration recoverable until finality (see [Transition window](#transition-window)). A node that cannot spare the disk can snap-sync the PBT at `PBT_ACTIVATION_FORK` instead of self-converting.

**No on-chain observability of the conversion.** This is the second cost. An overlay computes the new root in consensus, so its correctness is observed on-chain as it proceeds; offline conversion is not. This EIP recovers that visibility off consensus, through the [shadow-commitment period](#shadow-commitment). The substitute is weaker, because publication is not enforced: what gets observed is coverage over voluntary reports, not a validity condition every node must satisfy. One residual exposure remains, a converter bug confined to the final pre-fork blocks, whose activated PBT root consensus never validates. It is addressed under [Unvalidated-flip input](#unvalidated-flip-input).

### Why an anchor block and a single fork

Pinning conversion to one finalized block `ANCHOR_BLOCK` makes the conversion output a pure function of committed state. So independent producers can be required to agree bit-for-bit, and a fresh node can anchor trust in `ANCHOR_BLOCK`'s `stateRoot` without trusting any distributor. Collapsing activation into one fork `PBT_ACTIVATION_FORK` keeps the consensus change small and the swap semantics trivial (commitment only). That is what makes comprehensive rehearsal and full recoverability achievable.

### Why BAL-replay for catch-up

The conversion-node lineage this design descends from relied on ad-hoc catch-up messages. Replaying [EIP-7928](./eip-7928.md) Block-Level Access Lists instead applies per-block writes directly and needs no re-execution. BALs are already produced for other reasons and are already exchanged between peers by `eth/71` ([EIP-8159](./eip-8159.md)), so catch-up reuses an existing wire protocol instead of adding a migration-specific one. BALs do live outside the block body and are prunable, but re-anchoring keeps the range a node must replay inside the [EIP-7928](./eip-7928.md) retention window, so no extra retention rule is needed: see [BAL availability](#bal-availability).

Replay does need one thing execution does not: the ability to delete a leaf, specified under [replay deletion semantics](#replay-deletion-semantics). It follows from the invariant the whole verification story rests on, that converting the MPT at a block and replaying BALs onto an earlier snapshot must land on the same tree. Confining deletion to the pre-activation window keeps it out of the execution path, where [EIP-8297](./eip-8297.md)'s insert-and-update-only model still holds.

### Why shadow roots, and why publication is not a consensus rule

The offline model's weak point is that consensus does not observe conversion correctness before the swap. Having attesters publish signed per-block PBT roots through the pre-swap period restores that observability: divergence from the expected PBT root becomes visible and attributable while it is still harmless, so it can be caught and fixed before `PBT_ACTIVATION_FORK` rather than after. Attesters are the right source because they are the set that validates blocks, so the measurement covers the validating majority rather than whoever happens to build, and the validator signature is what makes a single report attributable.

Publication nonetheless stays a `SHOULD` and out of consensus. Enforcing it as a block-validity rule would make every validator compute the PBT post-state root for every block just to decide whether the block is valid. That puts PBT construction back on the consensus-critical path, the exact property the offline model exists to avoid, and it brings back the two-trees-consensus-live attack surface. Cross-client agreement therefore accumulates by observation rather than by enforcement, at the cost of coverage being a measured quantity instead of a guaranteed one.

That much is settled here; the wire specifics are not, and are left to the planned companion EIP. There is one narrow case where hard enforcement may still be warranted, the final blocks before `PBT_ACTIVATION_FORK`, discussed under [Unvalidated-flip input](#unvalidated-flip-input).

## Backwards Compatibility

`PBT_ACTIVATION_FORK` changes the state commitment only, so it is backwards compatible at the application layer:

- **Contracts.** No change. Contracts continue to address storage by 256-bit slot number via `SLOAD`/`SSTORE`; PBT key derivation runs inside the client, below the EVM. No Solidity/Yul changes are required.
- **Execution semantics.** Gas costs, opcode behavior, and transaction validity are unchanged across the swap itself; the commitment swap alone alters no observable execution result. PBT state access nonetheless performs differently from the MPT (e.g. chunk-granular code access, stem warm/cold behavior), so state-access gas costs are repriced in a **separate gas-repricing EIP**, planned but not yet published. That repricing must not be bundled into the commitment-only `PBT_ACTIVATION_FORK`; witness/statelessness pricing is likewise out of scope, as this migration does not introduce statelessness. The repricing fork's timing depends on benchmarks of PBT versus MPT access performance:
  - If PBT access is **more expensive** than the MPT, the repricing is a **price increase** and must activate at a fork **before** `PBT_ACTIVATION_FORK`. Running the PBT under the MPT-calibrated (lower) prices would leave the now-costlier operations underpriced, which is a denial-of-service surface.
  - If PBT access is **cheaper** than the MPT, the repricing is a **price decrease** and may activate at a fork **after** `PBT_ACTIVATION_FORK`, since charging the higher MPT-calibrated prices for cheaper PBT operations only overcharges and carries no denial-of-service risk.

The migration is **not** transparent to systems that depend on the state structure itself:

- **On-chain proof consumers.** The migration removes the per-account `storage_root` and changes the tree hash. So contracts and services that verify Merkle proofs of Ethereum state (bridges, light-client verifiers, `eth_getProof` consumers) require coordinated upgrades. This is the longest-lead-time dependency of the migration. No published EIP covers it, and it is expected to be taken up separately.
- **Node operators.** Nodes must obtain and verify the PBT snapshot (or self-convert) and must retain both trees through the transition window. BAL retention is unaffected: re-anchoring keeps catch-up inside the existing [EIP-7928](./eip-7928.md) window (see [BAL availability](#bal-availability)).

## Test Cases

<!-- TODO -->

## Security Considerations

### Unvalidated-flip input

At `PBT_ACTIVATION_FORK`, the PBT root activated for the final pre-fork block has not itself been validated by consensus. So a converter bug affecting only those final blocks could be activated. There are two mitigations. One is to hard-enforce the shadow root for the final blocks before `PBT_ACTIVATION_FORK`. That is the one place where making it a validity condition may be worth the cost. The other is to rely on sustained cross-client agreement accumulated over the shadow-commitment period. A correlated all-client conversion bug would be undetectable under either the online or the offline model, so this is not unique to this proposal.

### Shadow-root coverage

Because publication is voluntary, how much of the validating set reports depends on the [telemetry sidecar](#shadow-commitment) shipping enabled by default rather than on enforcement. Readiness assessment must therefore treat coverage as a measured quantity, and read a low-coverage period as weak evidence rather than as evidence of agreement. What is at stake is coverage, not representativeness: the reports come from the set that validates blocks, not only from the parties that build them. Pre-swap divergence itself stays harmless and self-detectable, since consensus still runs on the MPT.

### Distribution trust

The PBT snapshot is large (~100+ GB) and distributed off-chain, which in principle is a vector for serving corrupted state. The [dual-check](#verification-dual-check) neutralizes it: the root of trust is `ANCHOR_BLOCK`'s `stateRoot`, taken from the node's own header chain, so a fabricated but internally self-consistent package (fake leaves matching a fake `pbtRoot`) fails consensus anchoring, and fabricated code leaves fail its code limb. Integrity checking on partial downloads is left to the distribution transport (e.g. torrent piece hashes, Transport Layer Security (TLS), CDN checksums), letting a node drop a bad segment on arrival rather than after full reassembly; a malicious distributor serving altered bytes that pass those transport checks is still caught once the artifact is reassembled.

### Recoverability

A fault discovered during or immediately after the swap is recovered by falling back to the MPT, which the [transition window](#transition-window) keeps available until `PBT_ACTIVATION_FORK` is finalized.

## Copyright

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