EIP-8188 - State Tiering by Write Age

Created 2026-03-10
Status Draft
Category Core
Type Standards Track
Authors
Requires

Abstract

This proposal introduces renewal-age pricing for Ethereum state writes. It adds a last_written_period field to the RLP encoding of accounts and storage slots to track when each piece of state was last mutated. The gas cost of state-writing opcodes depends on whether the target state is in the Active or Inactive tier, determined by how many periods have elapsed since its last write. Writing to Inactive state is more expensive than writing to Active state. Read costs are unchanged.

This creates an economic incentive for state hygiene without mandating deletion, and provides clients with consensus-level metadata that identifies the recently mutated active set.

Motivation

Ethereum state continues to grow and its access costs are not uniform in practice:

  1. Renewal age is unpriced: Writing to state that has not been mutated in years costs the same as writing to state mutated recently, despite different resource profiles. Recently mutated state is more likely to stay in the performance-critical mutable working set, while older state more often falls onto slower paths with deeper database lookups and higher write overhead. A flat write cost overcharges for the active set and undercharges mutations to long-unmodified state.

  2. Abandoned state imposes uncompensated costs: Once state is written, it persists indefinitely. The ongoing storage, indexing, and lookup costs fall on node operators with no continuing contribution from the party that created it. A renewal-age-based surcharge introduces a continuing cost signal for long-unmutated state without requiring explicit in-protocol expiry or deletion.

  3. Clients lack consensus-visible write-age metadata: The protocol currently provides no record of when state was last mutated. The last_written_period field identifies the recently mutated active set, which every client can use as a signal for storage tiering.

The main objective of this proposal is to help clients optimize the commit-critical mutable path: trie updates, state-root computation, compaction, and other work associated with repeatedly rewriting state.

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 and RFC 8174.

Parameters

Name Value Description
PERIOD_LENGTH TBD Blocks per period (target: ~6 months)
PERIOD_START_BLOCK fork block Anchor block from which period numbering begins
INACTIVE_MIN_AGE TBD Periods of write-inactivity before state enters the Inactive tier

Tier Gas Constants

INACTIVE > ACTIVE MUST hold.

Constant Description
ACTIVE_ACCOUNT_WRITE Account mutation cost, Active tier
INACTIVE_ACCOUNT_WRITE Account mutation cost, Inactive tier
ACTIVE_STORAGE_WRITE Storage write cost, Active tier
INACTIVE_STORAGE_WRITE Storage write cost, Inactive tier

Period Definition

A global current_period is derived deterministically from the block number:

current_period = max(0, (block_number - PERIOD_START_BLOCK) // PERIOD_LENGTH)

All blocks within the same period share the same current_period value.

Encoding Changes

Account RLP Change

The account encoding is extended with a fifth field, last_written_period:

Before:

account = RLP([nonce, balance, storageRoot, codeHash])

After:

account = RLP([nonce, balance, storageRoot, codeHash, last_written_period])

Backwards compatibility: When decoding an account with only 4 RLP elements (pre-fork format), last_written_period MUST default to 0.

Storage Slot Encoding Change

Storage slot values are wrapped in a list to accommodate the new field:

Before:

slot_value = RLP(value)

After:

slot_value = RLP([value, last_written_period])

Backwards compatibility: The two formats are distinguished by their RLP prefix. A single bytestring has a prefix in the 0x80+ range, while a list has a prefix in the 0xc0+ range. Legacy slots (single bytestring) MUST default to last_written_period = 0.

Tier Determination and Gas Pricing

Renewal-age tiers

Given a state item's last_written_period, the renewal-age tier is determined by how many periods have elapsed since the last write:

period_age = current_period - last_written_period

if period_age < INACTIVE_MIN_AGE:
    tier = ACTIVE
else:
    tier = INACTIVE

Tiered write costs

The renewal-age tier determines the gas cost for write operations. Read operations are unaffected.

Operation Active Inactive
Account mutation ACTIVE_ACCOUNT_WRITE INACTIVE_ACCOUNT_WRITE
Storage mutation ACTIVE_STORAGE_WRITE INACTIVE_STORAGE_WRITE

Disambiguation from EIP-2929 warm/cold

Active and Inactive are renewal-age tiers determined by last_written_period. They affect write costs only. EIP-2929's warm/cold distinction, which is a within-transaction first-touch concept, continues to apply to reads as before. For writes, first determine the item's renewal-age tier, then apply the corresponding tiered write cost.

Period Update Rule

last_written_period is updated when a storage value is modified. The tier is evaluated before the write: writing to Inactive state pays the Inactive write cost, even though the write itself moves the item to Active. When period_age == 0 (i.e. same period as current_period), no period update occurs.

Storage slot rules

Account rules

Reads

Pure reads MUST NOT update last_written_period. This includes SLOAD, BALANCE, EXTCODESIZE, EXTCODECOPY, EXTCODEHASH, and call opcodes that do not transfer value.

Access List Interaction

Access lists (EIP-2930) pre-warm addresses and storage keys into the EIP-2929 accessed sets, removing transaction-local first-touch read overhead. Since read costs are not tiered, access-list read pricing is unaffected by this proposal.

However, access-list pre-warming MUST NOT erase the item's renewal-age tier for write purposes. If a transaction pre-warms a storage slot via an access list and later writes to it via SSTORE, the write MUST still pay the tiered write cost based on the slot's last_written_period.

Rationale

How can clients utilize the write-age metadata?

The last_written_period field identifies the recently mutated active set. Unlike local heuristics such as caches or snapshots, it is consensus-verified and consistent across all clients. This enables several categories of client-level optimization.

Mutable tier vs stable tier: From a client-storage perspective, Active and Inactive state can be viewed as a mutable tier and a stable tier. The protocol does not mandate any storage architecture, but the tier signal allows clients to isolate recently mutated state in a storage path optimized for repeated rewrites, while storing write-inactive state in a path optimized for density and read throughput.

Commit-path optimization: The main intended benefit is to help clients optimize the commit path: trie updates, state-root computation, compaction, and relocation associated with repeatedly rewriting state. The proposal aims to stop this write-critical mutable path from scaling with the entire state.

Storage separation: Clients MAY keep Active state in a smaller mutable store and Inactive state in a larger stable store, or use any equivalent architecture such as multiple column families, multiple databases, different compaction policies, or different storage media.

Prefetching on restart: When a node restarts, its in-memory caches are empty. With last_written_period, clients can scan the database on startup and selectively prefetch items whose period still places them in the Active tier, rebuilding the recent mutable working set without waiting for organic block processing.

None of these optimizations are mandated by this proposal. The last_written_period field enables but does not prescribe any particular client architecture. The gas pricing mechanism functions correctly regardless of whether clients implement these optimizations.

Why tier-based pricing?

A tier-based model with fixed gas constants per tier is used instead of a multiplier-based model.

Discrete tiers reflect the coarse resolution of the data, which is that state is either recently mutated or not recently mutated. Fixed constants are also easier for gas estimators to work with, such that an estimator only needs to determine whether the state falls into the Active or Inactive tier.

Why do reads not update the period?

This is a deliberate design choice. Only writes update last_written_period because:

Charging write cost for reads: If reads updated the period, every stale read would rewrite the trie leaf which escalates to state root computation. To price this properly, we would need to charge write-equivalent gas for what the caller expects to be a read. This makes stale reads prohibitively expensive. The alternative of charging read-level gas for an operation that actually performs a write underprices the real resource cost. Our empirical analysis also shows that there are more read-only operations than write operations, so gas cost increase in stale reads will have a significant impact on the overall ecosystem.

Database overhead: When reads bump the period, every stale read forces a trie leaf rewrite, which in turn forces database relocation from the stable tier to the mutable tier. This adds write amplification to what should be a read-only operation, contradicting the goal of isolating the mutable path.

STATICCALL correctness: EIP-214 guarantees that state is unchanged across a STATICCALL. Period metadata is part of consensus state (it affects the state root via trie hashing). A read that modifies period metadata would violate this guarantee.

How does extra metadata affect the state size?

The additional last_written_period field increases the size of every account and storage slot in the trie. For accounts, this adds one RLP-encoded integer (~1 byte for small period values). For storage slots, the encoding changes from a bare value to a two-element list, adding the period field plus a list prefix (~2 bytes total overhead).

As a rough estimate using current state sizes (as of early 2026):

Note that this is a worst-case figure assuming all state has been written at least once post-fork. In practice, pre-fork state that is never written post-fork retains the legacy encoding and incurs no overhead. Therefore, this proposal should have negligible impact in terms of increasing state size. However, the actual impact should be quantified during benchmarking across different clients.

How does this fit with state expiry?

This proposal is a weaker alternative to state expiry. It does not remove inactive state from consensus and therefore does not bound total state size. This differs from access-based state expiry. last_written_period tracks write-inactivity, not access-inactivity, and these are not equivalent sets. Under this proposal, a slot can be read constantly for years and still be classified as Inactive if it is never rewritten. Expiring such state would break live usage.

Is the Active tier bounded?

The Active tier is not strictly bounded by protocol, but is limited by gas over the renewal window.

For a tier window of W blocks, any Active item must have been written within that window, and each such refresh consumes gas. Therefore the maximum Active cardinality is bounded above by the total gas available in the window divided by the minimum marginal gas needed to refresh one additional item.

This is a worst-case upper bound. It assumes the protocol allows arbitrary refresh of any accounts or storage slots by any party. In practice, this bound is more natural for accounts than for storage slots, because not all storage slots can be refreshed independently by arbitrary actors.

The relevant claim for this proposal is not that the Active set is strictly bounded, but that it is economically rate-limited and therefore can remain much smaller than the total state under write-renewal semantics.

Relation with state creation cost (EIP-8037)

The gas cost of state creation SHOULD be at least equal to the Inactive tier write cost, and MAY be higher.

The rationale is that creating new state performs no less work than writing to Inactive state. Both operations require traversing and updating the state trie, including recomputing hashes along the modified path. However, state creation in the worst case could result in touching intermediate nodes that belong to some existing Inactive state.

For this reason, state creation MUST NOT be cheaper than the most expensive write tier. Otherwise, the gas schedule would create an inconsistency where allocating fresh state is cheaper than mutating old Inactive state.

Renewal gaming

There is an incentive to periodically rewrite state purely to keep it in the Active tier. This is by design, as the proposal intends to make state maintenance an ongoing cost borne by the parties who benefit from cheap future writes.

Backwards Compatibility

Security Considerations

Needs discussion.

Copyright

Copyright and related rights waived via CC0.