This proposal puts the last_written_block metadata defined in EIP-8188 directly into the leaves of the Partitioned Binary Tree (PBT) defined in EIP-8297.
For accounts, last_written_block is packed into the existing BASIC_DATA leaf. It takes the three reserved bytes plus one byte freed by narrowing code_size from four bytes in the original PBT design to three. For storage slots, the leaf value grows from 32 to 36 bytes: the unchanged 32-byte slot value, followed by a 4-byte big-endian last_written_block.
The rules that update last_written_block follow EIP-8188, with one difference: a storage write updates the slot's field only, not the account's. The metadata is part of each leaf's committed value, so anyone holding a leaf or a proof of it can retrieve the last_written_block of that account or storage slot at will. Apart from counting the 4 extra bytes of each storage leaf in state-creation gas (EIP-8037), this proposal introduces no gas changes.
EIP-8188 gives clients a consensus-verified record of when each account and storage slot was last written. State-tiering designs such as EIP-8295 build on that record. EIP-8188 defines it for the Merkle Patricia Trie (MPT): it adds a fifth element to the account RLP list and wraps each storage slot in a two-element RLP list.
Neither encoding carries over to the PBT. The PBT has no RLP. Account fields are packed at fixed byte offsets in BASIC_DATA, and a storage slot leaf holds only the raw 256-bit word.
This proposal defines where last_written_block lives in PBT leaves, so tiering can be driven directly from the tree. It records the raw block number, for the reason EIP-8188 gives: any coarser value a pricing scheme needs can be derived from the block number, but not the reverse.
Like EIP-8188, this proposal covers only the metadata. Apart from the EIP-8037 state-creation adjustment, it does not change gas costs or define a tiering policy.
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 and RFC 8174.
This proposal does not require EIP-8188. It defines last_written_block for the PBT completely and can activate whether or not EIP-8188 ever activates on the MPT. References to EIP-8188 are for comparisons only.
| Parameter | Value |
|---|---|
LAST_WRITTEN_BLOCK_SIZE | 4 |
ACCOUNT_LAST_WRITTEN_BLOCK_OFFSET | 1 |
STORAGE_LAST_WRITTEN_BLOCK_OFFSET | 32 |
STORAGE_LEAF_VALUE_LENGTH | 36 |
last_written_block is a fixed-width 4-byte big-endian unsigned integer. EIP-8188 stores the block number as an RLP integer, which has no fixed width: it uses as many bytes as the number needs, which is 4 for every current mainnet block, and would grow to 5 after block 2^32 - 1 without any protocol change. PBT leaves pack fields at fixed offsets, so this proposal fixes the width at 4 bytes (see "Why 4 bytes?" in the Rationale).
EIP-8297 packs the BASIC_DATA_LEAF_KEY value as follows:
| Name | Offset | Size |
|---|---|---|
version | 0 | 1 |
reserved | 1 | 3 |
code_size | 4 | 4 |
nonce | 8 | 8 |
balance | 16 | 16 |
This proposal changes it to:
| Name | Offset | Size |
|---|---|---|
version | 0 | 1 |
last_written_block | 1 | 4 |
code_size | 5 | 3 |
nonce | 8 | 8 |
balance | 16 | 16 |
nonce and balance keep their offsets and widths. code_size narrows to three bytes, which holds values up to 2^24 - 1 (16,777,215) bytes. That is about 256 times the 64 KiB code size limit of EIP-7954, so no valid code exceeds it.
EIP-8297 specifies that setting any header field also sets version and the three reserved bytes to zero. This proposal replaces that rule: setting any header field sets version to zero and sets last_written_block according to the update rules below. A header write MUST NOT change last_written_block in any other way.
Every storage slot leaf carries a value of STORAGE_LEAF_VALUE_LENGTH (36) bytes. This applies to header slots 0..63, stored at sub-index HEADER_STORAGE_OFFSET + storage_key in the account's header stem, and to slots 64 and above, stored in STORAGE_ZONE. Storage keys do not change.
| Name | Offset | Size |
|---|---|---|
slot_value | 0 | 32 |
last_written_block | 32 | 4 |
slot_value is the full 256-bit EVM storage word, unchanged and at the same offset it has in EIP-8297. last_written_block follows it, packed big-endian like the fields of BASIC_DATA.
Value length. EIP-8297's state_root asserts len(value) == 32 for every entry. This proposal replaces that assertion with len(value) == value_length(key). Merkelization does not change: leaf_hash = H(LEAF_TAG || key || value).
Zero and absence. EIP-8297 turns a write of 32 zero bytes into a deletion. For storage leaves, this proposal applies that test to slot_value only. A storage write whose slot_value is 32 zero bytes MUST delete the leaf, metadata included. As before, no storage leaf holds a zero slot_value, and reading an absent slot returns zero. Non-storage leaves are unaffected.
Account deletion. EIP-161 state clearing and same-transaction SELFDESTRUCT under EIP-6780 remove the account's header leaves and storage leaves, as specified in EIP-8297. Whether an account is empty depends on nonce, balance and code only. A nonzero last_written_block MUST NOT keep an otherwise empty account alive.
Non-empty storage check. The EIP-7610 check does not change: an address has non-empty storage exactly when a storage leaf exists for it.
last_written_block is set to the current block_number when a piece of state is mutated. Writing again within the same block is idempotent: the field already equals block_number, so nothing further changes.
The rules below follow EIP-8188, with one difference: a storage write does not update the account's last_written_block. A slot's field and its account's field are independent.
SSTORE nonzero → different nonzero): set the slot leaf's last_written_block = block_number.SSTORE to zero): the slot leaf is removed from the tree together with its metadata, so there is no slot field left to update.SSTORE same value): no change to the slot leaf.last_written_block = block_number.No storage slot rule changes the account's BASIC_DATA leaf. This holds for header slots 0..63 as well: they share the account's stem but are separate leaves.
last_written_block = block_number for both the sender and the receiver.last_written_block = block_number. This includes the authority nonce increment performed by each valid EIP-7702 authorization, so setting or clearing a delegation updates the authority's last_written_block.last_written_block = block_number.last_written_block = block_number.BASIC_DATA leaf is created with last_written_block = block_number.SELFDESTRUCT: follows the balance-transfer rule under EIP-6780. When the contract was not created in the same transaction, it is not removed, so set last_written_block = block_number on the self-destructing account and on the beneficiary whenever their balances change. There is no update when the beneficiary is the account itself, since its balance does not change. A contract created and destroyed in the same transaction is removed from the tree and carries no last_written_block. Only a surviving beneficiary whose balance changes is updated.The sender's fee payment needs no separate rule, because every transaction also increments the sender's nonce. Any other change to an account's balance or nonce falls under the general rule at the top of this section.
Pure reads MUST NOT update last_written_block. This includes SLOAD, BALANCE, EXTCODESIZE, EXTCODECOPY, EXTCODEHASH, and call opcodes that do not mutate state. Retrieving last_written_block itself (see Retrieval) is also a pure read.
When a call frame reverts, last_written_block is restored to its value from before the frame, exactly like balance, nonce and storage. Because the field sits in the same leaf value as the state it dates, it is journaled and reverted with that leaf. Implementations MUST NOT keep a last_written_block update whose accompanying state change was rolled back.
Code-zone leaves, CODE_HASH_LEAF_KEY leaves and DELEGATION_LEAF_KEY leaves carry no last_written_block of their own. This matches EIP-8188, which dates only accounts and storage slots. Those leaves change only alongside account creation or a nonce increment, and both of those update the account's BASIC_DATA.
The last_written_block of any account or storage slot can be retrieved at will. It is part of the leaf's committed value, so reading it needs only the leaf: no transaction execution, no side index and no state change.
Retrieval has these properties:
last_written_block or any other state.last_written_block is covered by leaf_hash, so a standard EIP-8297 inclusion proof for a leaf also proves that leaf's last_written_block against the state root. Light clients and stateless verifiers can check it without trusting the node that served it.0 (see Backwards Compatibility).Like EIP-8188, this proposal adds no EVM opcode, and the field stays invisible to contracts. Retrieval happens at the client, RPC and proof level. Clients MAY expose it through JSON-RPC; standardizing such a method is out of scope.
STATE_BYTES_PER_STORAGE_SET, defined in EIP-8037, MUST be increased by LAST_WRITTEN_BLOCK_SIZE (4) bytes, so that state-creation gas tracks the growth of each storage leaf. STATE_BYTES_PER_NEW_ACCOUNT does not change, because account leaves do not grow.
This proposal SHOULD activate in the same fork as EIP-8297.
BASIC_DATA?Every account access already opens the BASIC_DATA leaf. Putting last_written_block there means reading or updating an account's write age needs no extra branch opening, and accounts grow by zero bytes, compared with 5 bytes per account under EIP-8188. One of the reserved header sub-indices between CODE_HASH_LEAF_KEY and HEADER_STORAGE_OFFSET could hold a separate field instead, but that would add one leaf per account.
EIP-8188 encodes last_written_block as an RLP integer, which uses only as many bytes as the number needs. Every current mainnet block number needs 4. EIP-8188 notes that the field would need a fifth byte only after block 2^32 - 1 (about 4.29 billion), which is about 1,600 years away at 12-second slots and about 800 years at 6-second slots. Under RLP, that growth happens automatically.
PBT leaves have no RLP. Fields sit at fixed offsets, so last_written_block needs a fixed width, chosen once. Three bytes is too few: 2^24 - 1 = 16,777,215 is already below mainnet's block height. Four bytes is the most BASIC_DATA can give without taking space from nonce or balance. A fifth byte would have to come from code_size, and a 2-byte code_size tops out at 65,535, just under the 64 KiB code size limit. Storage leaves could hold a wider field, but they use the same 4 bytes so that an account and its slots store the block number the same way.
Unlike EIP-8188's field, this one cannot grow on its own. A block number of 2^32 or above cannot be encoded, so a later fork must widen the field, for example by moving it into its own header leaf, before mainnet reaches block 2^32.
code_size?BASIC_DATA has only three reserved bytes, so the fourth byte of last_written_block has to come from an existing field. nonce is 8 bytes because EIP-2681 caps nonces at 2^64 - 1, so narrowing it would change a consensus limit. balance could give up a byte in practice, but its width is not tied to any protocol limit, so narrowing it would introduce a new consensus bound on balances. code_size is the only field whose range is already capped by a protocol limit far below its width.
A storage slot holds a full 256-bit EVM word, and every byte of it is meaningful. There are no reserved bytes to reuse, and taking bits from the word would change what a contract can store. The alternatives are to widen the leaf or to add a parallel metadata leaf. A parallel leaf doubles the storage leaf count, needs its own key and branch, and must be kept in lockstep with the slot. Widening keeps the metadata inside the same leaf, so one read, one write and one proof cover both, and the metadata is deleted, reverted and migrated together with the slot automatically.
The leaf needs exactly the 32-byte slot value and the 4-byte block number, so the value is 36 bytes. Across about 1.5 billion slots (EIP-8188's early-2026 estimate), that adds about 6 GB of raw data, less than the roughly 9 GB EIP-8188 adds with its 6-byte RLP framing.
The slot value comes first, so it stays at offset 0, where EIP-8297 puts it. Code that reads slot content takes the first 32 bytes, as before.
With the BLAKE3 hash used by EIP-8297's reference implementation, the extra 4 bytes do not add a compression. A leaf's hash preimage is LEAF_TAG || key || value: 71 bytes for a header slot and 103 bytes for a storage-zone slot. Each still fits in two 64-byte blocks, the same as with a 32-byte value. The merkelization hash is not final, so this should be rechecked once it is chosen.
In EIP-8188, a storage write also updates the account's last_written_block. That follows from the MPT's shape: the account RLP contains storageRoot, so every storage write rewrites the account leaf anyway, and the field records that rewrite.
The PBT has no storage_root. An account's header and its storage slots are separate leaves, and a storage write never touches BASIC_DATA. A cascade would add a BASIC_DATA rewrite that the tree does not otherwise perform, in every block in which a contract's storage changes.
Without the cascade, each field dates its own leaf. The account's field records the last write to the header (balance, nonce, code), and each slot's field records the last write to that slot. In both trees, last_written_block is therefore the last block in which that leaf was written.
The cost is that an account's field no longer covers its storage, as it does under EIP-8188. A tiering rule that classifies each leaf by its own last_written_block applies directly. A rule that prices an SSTORE through the account leaf has no account write to price on the PBT.
As in EIP-8188, the block number is the finest-grained primitive. Any coarser value a tiering or pricing scheme needs, such as the periods of EIP-8295, can be derived from it deterministically. Storing the finest granularity keeps this encoding independent of pricing parameters that are still being decided.
BASIC_DATA layout, the storage leaf value length and the storage deletion test, all of which affect the state root.last_written_block = 0. A leaf gets a real block number only at its first write after activation.BASIC_DATA leaf, because code_size moves, and every storage leaf, because its value widens.SLOAD returns slot_value only. BALANCE, EXTCODESIZE, EXTCODECOPY and EXTCODEHASH are unchanged, and code_size keeps its meaning. No contract code changes.BASIC_DATA layout and the 36-byte storage leaf value.The merkelization hash is not final, so digests are not pinned. H(x) denotes the 32-byte key_hash of x. Block numbers are illustrative. The example follows one contract, A, with 1,234 bytes of code and nonce 1.
Block 30,000,000: A receives 1 ETH. This is a nonzero balance transfer, so A's last_written_block is set.
BASIC_DATA key: 0x00 || H(A) || 0x00
| Field | Offset | Bytes | Decoded |
|---|---|---|---|
version | 0 | 00 | 0 |
last_written_block | 1 | 01 C9 C3 80 | 30,000,000 |
code_size | 5 | 00 04 D2 | 1,234 |
nonce | 8 | 00 00 00 00 00 00 00 01 | 1 |
balance | 16 | 00 00 00 00 00 00 00 00 0D E0 B6 B3 A7 64 00 00 | 10^18 wei (1 ETH) |
Block 30,000,500: one transaction sets slot 5 from 0 to 42 and slot 1000 from 7 to 8.
Slot 5 lives in the header stem, since 5 < 64. It is a new slot.
0x00 || H(A) || 0x45 (sub-index 64 + 5 = 69)00 × 31, 2A | 01 C9 C5 74Slot 1000 lives in the storage zone: tree_index = 1000 // 256 = 3 and sub_index = 1000 % 256 = 232. It is a value change.
0xFF || H(A) || H(A || 3) || 0xE800 × 31, 08 | 01 C9 C5 74Neither write touches A's BASIC_DATA leaf. Its last_written_block stays at 30,000,000.
Block 30,000,600: reads and a no-op write. A transaction calls SLOAD on slot 1000, BALANCE(A) and EXTCODESIZE(A), and writes 42 to slot 5 again. Nothing changes: the reads are pure, and the write is a no-op.
Block 30,000,700: slot 1000 is set to 0. The slot leaf at 0xFF || H(A) || H(A || 3) || 0xE8 is deleted together with its metadata. A's BASIC_DATA leaf is unchanged.
Block 30,000,800: a reverted write. A call frame writes 43 to slot 5 and then reverts. The slot 5 leaf keeps slot_value = 42 and last_written_block = 30,000,500.
Block 30,000,900: A sends 0.25 ETH. This is a nonzero balance transfer, so A's BASIC_DATA bytes 1..4 become 01 C9 C7 04 (30,000,900). No storage leaf changes.
Retrieval after block 30,000,900:
| Query | Leaf read | Result |
|---|---|---|
get_account_last_written_block(A) | 0x00 || H(A) || 0x00, bytes 1..4 | 30,000,900 |
get_storage_last_written_block(A, 5) | 0x00 || H(A) || 0x45, bytes 32..35 | 30,000,500 |
get_storage_last_written_block(A, 1000) | absent | None (slot is zero) |
Preimage injectivity. EIP-8297's injectivity argument rests on the node tags, the explicit bit count in branch prefixes, and one fixed key length per zone. Under this proposal, a leaf's value length is a function of its key: the zone byte, and within ACCOUNT_ZONE the sub-index. LEAF_TAG || key || value therefore still parses uniquely, and no two distinct logical leaves share a preimage.
State growth. This proposal adds no new write operations. The field changes only in a leaf that the same operation is already writing, so every update rides on a write that is already paid for. The 4 additional bytes per storage leaf are real state growth. The State-creation gas rule in the Specification prices them, so new slots are not underpriced relative to their encoded size.
Witness size. Proofs of storage slots grow by 4 bytes per proven slot, because the leaf value is larger. Account proofs are unchanged.
STATICCALL correctness. last_written_block is consensus state and reads never update it, so the EIP-214 guarantee that state is unchanged across a STATICCALL holds.
Copyright and related rights waived via CC0.