EIP-8266 - Expiring Nonces for Frame Transactions

Created 2026-05-15
Status Draft
Category Core
Type Standards Track
Authors
Requires

Abstract

Adds an "expiring nonce" mode to EIP-8141 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.

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 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:

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:

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:

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 accessed_addresses or accessed_storage_keys, are NOT charged under EIP-2200 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:

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