---
eip: 8266
title: Expiring Nonces for Frame Transactions
description: Short-lived replay protection for frame transactions via a sig-hash ring buffer
author: Toni Wahrstätter (@nerolation), lightclient (@lightclient)
discussions-to: https://ethereum-magicians.org/t/eip-8266-expiring-nonces-for-frame-transactions/28575
status: Draft
type: Standards Track
category: Core
created: 2026-05-15
requires: 8141
---

## Abstract

Adds an "expiring nonce" mode to [EIP-8141](./eip-8141.md) frame transactions in which replay protection is bounded by a short transaction deadline rather than the sender's account nonce. Once a deadline passes, the slot used to track that transaction is freed and reused, so unlike account or keyed nonces the scheme adds no permanent state growth.

## Motivation

EIP-8141 enforces per-sender serialization through a linear account nonce. For transactions intended to be short-lived (atomic intents, time-boxed sponsorships) this is unnecessarily restrictive: such transactions either succeed within seconds or become irrelevant, so the only replay risk worth defending against is rebroadcast inside that window. A linear nonce forces the sender to commit to an ordering, prevents multiple pending transactions, and ties inclusion order to nonce order.

Expiring nonces replace this with a soft replay window: a transaction is non-replayable only as long as its deadline is in the future. After the deadline, the slot it occupied is freed and may be reused. State is bounded by ring capacity, not by transaction volume.

## 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.

This specification is a delta against [EIP-8141](./eip-8141.md).

### Constants

| Name | Value |
|---|---|
| `FORK_TIMESTAMP` | `TBD` |
| `NONCE_RING` | `0xTBD` |
| `NONCE_RING_CODE` | `0x60006000fd` |
| `EXPIRING_NONCE_SENTINEL` | `2**64 - 1` |
| `MAX_EXPIRY_SECS` | `60` |
| `RING_CAPACITY` | `262144` (`2**18`) |
| `EXPIRING_NONCE_GAS` | `13000` |

`NONCE_RING_CODE` is a runtime equivalent to `revert(0, 0)`: any ordinary call to `NONCE_RING` reverts with empty returndata.

`NONCE_RING` MUST be selected so that no code or storage exists at the address on every intended activation network at fork-configuration finalization.

### Mode selection

A frame transaction is in **expiring-nonce mode** if `tx.nonce == EXPIRING_NONCE_SENTINEL`. Otherwise the transaction follows [EIP-8141](./eip-8141.md) unchanged.

In expiring-nonce mode the protocol does not read or write `state[tx.sender].nonce` for replay protection.

### Storage layout

Let `h = compute_sig_hash(tx)` denote the canonical signature hash. State in `NONCE_RING` is defined by three slot families:

```text
slot_seen(h)     = keccak256(h || left_pad_32(0))     # uint64 deadline, 0 means absent
slot_ring(i)     = keccak256(left_pad_32(1)) + i      # bytes32 sig-hash, 0 means absent
slot_ring_ptr    = left_pad_32(2)                     # uint64 monotonic counter
```

`i` ranges over `[0, RING_CAPACITY)`. Slots not yet written read as `0`.

### Stateful validity

Let `now = block.timestamp`. Let `expiry_frames` be the list of frames in `tx.frames` with `mode == VERIFY` and `target == EXPIRY_VERIFIER`. A post-fork frame transaction in expiring-nonce mode is statefully valid only if:

```python
assert len(expiry_frames) == 1
d = int.from_bytes(expiry_frames[0].data, "big")            # 8-byte big-endian deadline
assert now <= d <= now + MAX_EXPIRY_SECS
assert uint64(state[NONCE_RING].storage[slot_seen(h)]) < now
```

The sender-nonce check from EIP-8141 (`tx.nonce == state[tx.sender].nonce`) is skipped.

### Nonce consumption

EIP-8141's payment-approval transition currently increments the sender's account nonce. In expiring-nonce mode that increment is replaced with the following sequence, performed atomically on the unique successful payment-scoped `APPROVE`:

```python
def consume_expiring_nonce(h, d, now):
    storage = state[NONCE_RING].storage
    ptr     = uint64(storage[slot_ring_ptr])
    idx     = ptr % RING_CAPACITY
    oldHash = bytes32(storage[slot_ring(idx)])

    if oldHash != bytes32(0):
        oldDeadline = uint64(storage[slot_seen(oldHash)])
        if oldDeadline >= now:
            raise BufferFull                          # halts APPROVE; no approval effects
        storage[slot_seen(oldHash)] = 0

    storage[slot_ring(idx)] = h
    storage[slot_seen(h)]   = d
    storage[slot_ring_ptr]  = ptr + 1
```

A payment-scoped `APPROVE` in expiring-nonce mode additionally deducts `EXPIRING_NONCE_GAS` from the executing frame, after EIP-8141's ordinary `APPROVE` checks and opcode costs and before any approval effect is committed. If the frame has less gas remaining than `EXPIRING_NONCE_GAS`, the `APPROVE` halts out-of-gas and no approval effects occur. If `consume_expiring_nonce` raises `BufferFull`, the `APPROVE` halts with no approval effects, the transaction's `payer` is never set, and the transaction is invalid under EIP-8141.

`consume_expiring_nonce` runs exactly once per transaction. Its effects are approval effects under EIP-8141: they are journaled outside the current frame's revert journal and outside any `SENDER` atomic-batch snapshot, and MUST NOT be reverted by a later frame revert or atomic-batch rollback.

Reads and writes performed by stateful validity and `consume_expiring_nonce` are protocol bookkeeping: they do NOT add `NONCE_RING` or its slots to [EIP-2929](./eip-2929.md) `accessed_addresses` or `accessed_storage_keys`, are NOT charged under [EIP-2200](./eip-2200.md) `SSTORE` pricing, and do NOT warm the address or slot for later user-level access.

`EXPIRING_NONCE_GAS = 13,000` prices the per-consume access and mutation work but deliberately omits the `SSTORE_SET` premium that ordinary EVM accounting would charge for the zero-to-non-zero write of `slot_seen(h)`. The premium prices permanent state growth, which `consume_expiring_nonce` does not produce: every fresh `slot_seen(h_new)` write is paired within the same transition with a `slot_seen(h_old) = 0` clear of the entry leaving the ring, so the storage trie's leaf count is invariant in steady state. Total `NONCE_RING` footprint is fixed at exactly `2 × RING_CAPACITY` slots regardless of usage; the one-time cost of filling the ring during the first `RING_CAPACITY` consumes is a bootstrap cost absorbed by the protocol rather than charged per-tx.

### Mempool

In addition to EIP-8141's mempool rules, a node MUST:

* reject an expiring-nonce transaction if any stateful-validity assertion above fails;
* re-evaluate those assertions on every new head and evict failures.

Nodes MAY admit multiple pending expiring-nonce transactions per sender; EIP-8141's one-pending-frame-transaction-per-sender guidance does not apply. Instead, the node MUST reserve each pending transaction's maximum cost (`TXPARAM(0x06)`) against its gas payer's available balance, applying EIP-8141's `reserved_pending_cost(payer)` accounting, and admit a new transaction only if the payer's available balance covers it.

### Activation

This EIP MUST activate at or after [EIP-8141](./eip-8141.md). At activation, on the first execution payload with `timestamp >= FORK_TIMESTAMP` and before any transaction in that payload runs, clients MUST install `NONCE_RING_CODE` at `NONCE_RING` with nonce `1` and empty storage, preserving any pre-existing balance. The same address-selection and existing-account-handling requirements as EIP-8141's `EXPIRY_VERIFIER` activation apply.

Pre-fork frame transactions and authorizations bound to the pre-fork canonical signature hash do not survive the boundary and MUST be evicted from mempools and regenerated.

## Rationale

### Sentinel rather than envelope flag

The sentinel reuses an existing field whose range (`< 2**64`) already covers `EXPIRING_NONCE_SENTINEL`. No payload schema change is required, the canonical signature hash continues to commit to the mode marker through the existing `nonce` field, and the design composes cleanly with [EIP-8250](./eip-8250.md) (see below).

### Reusing `expiry_verify`

EIP-8141 already defines the `expiry_verify` frame: canonical contract, 8-byte big-endian calldata, and a signature-hash exception that makes the deadline sender-authenticated. Adding a parallel deadline field on the envelope would duplicate all of that. Reusing the existing frame keeps the signature-hash procedure unchanged.

### Ring buffer instead of permanent per-tx slots

A naive design, writing `seen[h] = d` and never clearing, grows state without bound. Pricing that growth via the zero-to-nonzero `SSTORE` cost would push per-tx cost above what short-lived sends should pay. A fixed-capacity ring bounds total state at exactly `2 × RING_CAPACITY` slots, removes the need for an `SSTORE_SET` surcharge, and lets this EIP charge a flat `EXPIRING_NONCE_GAS = 13000` covering the ring's read and write set.

### `MAX_EXPIRY_SECS` bound

`MAX_EXPIRY_SECS` exists to keep the invariant `MAX_EXPIRY_SECS × peak_tps ≤ RING_CAPACITY`. With `MAX_EXPIRY_SECS = 60` (five Ethereum slots) and `RING_CAPACITY = 2**18`, the ring tolerates ~4369 sustained expiring-nonce TPS before eviction can race a live deadline. Without an upper bound, an attacker could pin a slot far into the future, and once the ring wrapped past it, a fresh hash would evict an entry whose deadline was still live, opening a replay window.

### `BufferFull` as defense-in-depth

If the sizing invariant above holds, `BufferFull` is unreachable. It is kept as a hard stop so a brief load spike, a mis-sized future fork, or an unanticipated workload cannot silently downgrade the replay guarantee: rather than evict a still-live entry, the protocol fails the offending approval.

### Composition with EIP-8250

If both this EIP and [EIP-8250](./eip-8250.md) ship, the sentinel collapses into EIP-8250's keyed-nonce framing as a reserved key `nonce_key == 2**256 - 1`; `NONCE_RING`'s storage can live in `NONCE_MANAGER` under a distinct slot prefix; and the mechanism in this EIP is otherwise unchanged. This composition is non-normative.

## Backwards Compatibility

Transactions with `tx.nonce != EXPIRING_NONCE_SENTINEL` behave exactly as in EIP-8141. Existing wallets, RPC methods, and verifiers continue to work unchanged. Verifiers that previously assumed `TXPARAM(0x01)` is the sender's account nonce MUST either reject expiring-nonce transactions or be updated to handle the sentinel.

## Security Considerations

The replay window of an expiring-nonce transaction is its deadline `d`. Rebroadcasts after `d` are rejected by the `expiry_verify` frame itself; rebroadcasts at or before `d` are rejected by the `seen[h].deadline < now` check (a strict inequality is required to block same-block reuse when `d == block.timestamp`). The ring guarantees that an entry is not evicted while still live as long as `MAX_EXPIRY_SECS × peak_tps ≤ RING_CAPACITY`; `BufferFull` enforces this invariant at execution time.

Because expiring-nonce transactions do not advance the sender's legacy account nonce, an EOA can have many pending expiring-nonce transactions plus an unrelated legacy-nonce transaction outstanding at the same time. Single-use applications that derive intent from the legacy nonce SHOULD authenticate the pre-state legacy nonce explicitly rather than relying on transaction ordering.

`NONCE_RING` storage is system-managed; ordinary direct calls revert. Any balance held at `NONCE_RING` is outside the scope of this EIP and is not recoverable by protocol logic defined here.

## Copyright

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