EIP-8250 - Keyed Nonces for Frame Transactions

Created 2026-04-16
Status Draft
Category Core
Type Standards Track
Authors
Requires

Abstract

Replaces the single sender nonce of an EIP-8141 frame transaction with a (nonce_key, nonce_seq) pair. nonce_key == 0 aliases the legacy account nonce; each non-zero key selects an independent protocol-managed nonce sequence stored in a NONCE_MANAGER system contract. Transactions on different non-zero keys are replay-independent.

Motivation

A frame transaction currently consumes one linear sender nonce, so a delayed transaction blocks every later frame transaction from the same sender. This is too restrictive for designs that intentionally share one sender across many independent users or actions.

The leading example is privacy protocols. To avoid binding onchain activity to a unique public sender, users transact through a single shared sender address. With one linear nonce, that shared sender becomes a throughput bottleneck: one user's inclusion invalidates every other user's pending withdrawal even when the spends are otherwise unrelated. Smart-wallet session keys and relayer-style senders face the same problem.

Keyed nonces let each spend pick its own nonce domain, for example one derived from a privacy nullifier. Transactions on different keys are replay-independent. This removes the protocol-level nonce obstacle to future keyed-aware mempools that admit concurrent transactions from the same sender. This EIP does not by itself change EIP-8141's public-mempool guidance.

Tying nonce consumption to EIP-8141's payment-approval step also gives single-use-key applications, such as nullifiers, an atomic spent-once guarantee: if validation requires the selected key to be unused, successful inclusion makes it used, regardless of whether later frames revert.

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_MANAGER 0xTBD
NONCE_MANAGER_CODE 0x60006000fd
KEYED_NONCE_FIRST_USE_GAS 20000
MAX_NONCE_SEQ 2**64 - 1
TXPARAM_NONCE_KEY 0x0B
TXPARAM_LEGACY_NONCE 0x0C

NONCE_MANAGER_CODE is a runtime equivalent to revert(0, 0): any ordinary call to NONCE_MANAGER immediately reverts with empty returndata.

NONCE_MANAGER MUST be selected so that no code or storage exists at the address on every intended activation network at fork-configuration finalization. If the selected address has non-empty code or non-empty storage on any intended activation network at fork-configuration finalization, a different address MUST be selected. This EIP does not define a normal activation transition that clears non-empty storage or overwrites non-empty code at NONCE_MANAGER.

Transaction payload

The frame-transaction payload becomes:

[chain_id, nonce_key, nonce_seq, sender, frames,
 max_priority_fee_per_gas, max_fee_per_gas,
 max_fee_per_blob_gas, blob_versioned_hashes]
````

`nonce_key` is a `uint256`; `nonce_seq` is a `uint64`. Both MUST be canonical minimal-length RLP integers. The frame layout, signature-hash procedure with `VERIFY`-frame `data` elided, and all other EIP-8141 transaction fields are unchanged.

Decoders MUST reject a post-fork `FRAME_TX_TYPE` transaction if any of the following is true:

* the payload does not match the post-fork schema;
* `nonce_key` or `nonce_seq` is not encoded as a canonical RLP integer;
* `nonce_key >= 2**256`;
* `nonce_seq >= 2**64`.

### Nonce state

For `nonce_key == 0`, the selected nonce domain is the sender's legacy account nonce.

For `nonce_key != 0`, the selected nonce domain is stored in protocol-managed storage under `NONCE_MANAGER`.

Let:

```text
A = left_pad_32(sender)
K = uint256_to_bytes32(nonce_key)

Then:

slot(sender, nonce_key) = keccak256(A || K)

Equivalently, slot(sender, nonce_key) is the keccak256 hash of the 32-byte left-padded sender address followed by the 32-byte big-endian encoding of nonce_key.

def current_nonce_seq(sender, nonce_key):
    if nonce_key == 0:
        return state[sender].nonce
    return uint256(state[NONCE_MANAGER].storage[slot(sender, nonce_key)])

For nonce_key != 0, an absent slot reads as 0. The protocol never writes 0: sequence 0 is represented only by an absent slot, so first use of a key is detectable as a zero-valued read. Only the protocol writes keyed-nonce slots; ordinary calls to NONCE_MANAGER revert.

Stateful validity

Let tx_legacy_nonce be the value of state[tx.sender].nonce observed from the transaction's actual pre-state within block execution order, before any frame executes.

A post-fork frame transaction is statefully valid only if:

assert tx.nonce_seq < MAX_NONCE_SEQ
assert tx.nonce_seq == current_nonce_seq(tx.sender, tx.nonce_key)

This check occurs at the same stage as EIP-8141's existing nonce check, before any frame executes.

Stateful validity is evaluated against the transaction's actual pre-state within block execution order. Therefore, two frame transactions using the same (sender, nonce_key) are valid in one block only if each transaction's nonce_seq equals the current sequence at its position in block execution order.

Nonce consumption

EIP-8141's payment-approval transition currently increments the sender's account nonce. This EIP replaces that increment with:

def consume_nonce(sender, nonce_key, nonce_seq):
    if nonce_key == 0:
        increment_account_nonce(sender)
    else:
        state[NONCE_MANAGER].storage[slot(sender, nonce_key)] = nonce_seq + 1

consume_nonce runs exactly once per transaction, on the unique successful payment-scoped APPROVE that sets payer_approved = true.

A payment-scoped APPROVE is an APPROVE whose scope includes APPROVE_PAYMENT, i.e. scope 0x1 or 0x3.

For nonce_key == 0, increment_account_nonce(sender) increments the sender's current account nonce; it does not set the account nonce to tx.nonce_seq + 1. This preserves EIP-8141 behavior if an earlier frame in the same transaction has already changed the sender's legacy nonce, for example by account deployment or by executing CREATE or CREATE2 at tx.sender.

If nonce_key == 0 and increment_account_nonce(sender) would make state[sender].nonce > MAX_NONCE_SEQ, the payment-scoped APPROVE fails with an exceptional halt and performs no approval effects.

For nonce_key != 0, stateful validity guarantees tx.nonce_seq < MAX_NONCE_SEQ, so tx.nonce_seq + 1 is in range.

For a payment-scoped APPROVE, clients first apply all EIP-8141 APPROVE exceptional-condition checks and ordinary opcode execution costs, including any RETURN-like memory costs, but excluding the legacy nonce increment that this EIP replaces.

Only after those checks and costs succeed, and before any approval effect is committed, clients MUST perform:

  1. If tx.nonce_key != 0, read raw_before = state[NONCE_MANAGER].storage[slot(tx.sender, tx.nonce_key)]. Otherwise skip to step 4.
  2. If raw_before == 0 and the current frame has less than KEYED_NONCE_FIRST_USE_GAS gas remaining, the APPROVE halts out-of-gas and no approval effects occur.
  3. If raw_before == 0, deduct KEYED_NONCE_FIRST_USE_GAS from the current frame's remaining gas.
  4. Execute consume_nonce(tx.sender, tx.nonce_key, tx.nonce_seq).
  5. Commit the remaining EIP-8141 payment-approval effects, excluding the legacy nonce increment replaced above.

Steps 3 through 5 are a single approval transition. Either all are committed, or none are committed.

Nonce consumption, maximum-cost collection, payer recording, first-use gas charging, and approval-flag updates performed by a successful payment-scoped APPROVE are approval effects, not ordinary frame-local state changes. They MUST be journaled outside the current frame's revert journal and outside any SENDER atomic-batch snapshot. They MUST NOT be reverted by a later frame revert, by skipping later frames, or by restoring an atomic-batch state snapshot.

Gas deducted as KEYED_NONCE_FIRST_USE_GAS is gas used by the frame executing APPROVE. When charged, the surcharge is included in the frame's gas_used receipt value, transaction gas accounting, and EIP-8141 unpaid-gas refund calculation.

Keyed-nonce reads and writes performed by stateful validity and consume_nonce are protocol bookkeeping: they do NOT add NONCE_MANAGER 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.

TXPARAM

TXPARAM(0x01) returns tx.nonce_seq. Two new indices are added:

param Return value
0x0B tx.nonce_key
0x0C pre-state legacy sender nonce

TXPARAM(0x0C) returns tx_legacy_nonce, the value of state[tx.sender].nonce observed during stateful validity before any frame executes. It is transaction-scoped and is not updated by payment approval, keyed-nonce consumption, account deployment, CREATE, or CREATE2 within the same transaction.

For nonce_key == 0, stateful validity requires TXPARAM(0x01) == TXPARAM(0x0C) at transaction start. For nonce_key != 0, they may differ.

Activation

This EIP MUST activate at or after EIP-8141.

If timestamp < FORK_TIMESTAMP, clients MUST apply the pre-fork EIP-8141 FRAME_TX_TYPE schema and MUST NOT apply keyed-nonce logic.

If timestamp >= FORK_TIMESTAMP, clients MUST apply the post-fork FRAME_TX_TYPE schema defined in this EIP.

At activation, on the first execution payload with timestamp >= FORK_TIMESTAMP and before any transaction in that payload runs, clients MUST initialize NONCE_MANAGER with NONCE_MANAGER_CODE, nonce 1, and empty storage, preserving any pre-existing balance.

If NONCE_MANAGER does not exist, clients MUST create it with balance 0, nonce 1, code NONCE_MANAGER_CODE, and empty storage.

If NONCE_MANAGER already exists with empty code and empty storage, clients MUST set its code to NONCE_MANAGER_CODE, set its nonce to max(existing_nonce, 1), preserve its balance, and leave storage empty.

This initialization runs exactly once at fork activation and MUST NOT be re-applied during normal chain progression after activation. Clients MUST handle reorgs across the fork boundary by applying or undoing this transition according to the canonical chain.

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

A contract-managed nullifier table inside a VERIFY frame is not sufficient: VERIFY is static-call-like and cannot write ordinary contract state, and deferring the spent-mark to a later SENDER frame breaks atomicity once payment approval can persist through later-frame failure. Lifting nonce consumption into the payment-approval transition gives a single, atomic spent-once guarantee.

nonce_key == 0 aliases the legacy account nonce so EIP-8141's existing replay behavior is preserved. Account deployment, CREATE, and CREATE2 continue to affect account nonces by ordinary EVM rules. Only payment-approval nonce-bumping is replaced.

nonce_key is uint256 because privacy protocols often derive keys from 32-byte nullifiers, commitments, or hash outputs. ERC-4337 uses the same key/sequence model but has only one 32-byte nonce field, so it packs a 24-byte key and an 8-byte sequence into that field. This EIP uses two explicit fields: a 32-byte nonce_key and an 8-byte nonce_seq. That keeps ERC-4337's 64-bit sequence width, avoids truncating nullifier-derived labels, and makes ERC-4337-style 24-byte keys a subset of this EIP's key space.

Keyed-nonce state lives in the storage of one NONCE_MANAGER system contract rather than extending account state, since the latter would change the account MPT layout. This minimizes consensus-surface change while keeping keyed state committed under the existing execution stateRoot.

KEYED_NONCE_FIRST_USE_GAS = 20000 uses the zero-to-nonzero SSTORE state-creation cost as a reference point. It is a keyed-nonce state-growth surcharge, not ordinary user-level SSTORE pricing. Subsequent increments of a consumed key are not separately surcharged: keyed-nonce progression is replay-protection bookkeeping analogous to legacy-nonce progression, not user storage.

This EIP does not relax EIP-8141's public-mempool one-pending-frame-transaction-per-sender guidance, but it removes the protocol-level obstacle to doing so. A future policy MAY admit multiple pending frame transactions for the same sender on distinct non-zero keys, subject to its own dependency-tracking and replacement rules.

Backwards Compatibility

nonce_key == 0 preserves EIP-8141 replay behavior. Legacy account objects, non-frame transaction types, and eth_getTransactionCount are unchanged.

TXPARAM(0x01) continues to return the transaction's replay-protection sequence, which equals the sender's legacy nonce only when nonce_key == 0. Verifier code that previously assumed TXPARAM(0x01) is the legacy account nonce MUST either enforce TXPARAM(0x0B) == 0 or be updated to handle keyed sequences.

TXPARAM(0x0C) provides explicit pre-state legacy-account-nonce access for verifier code that needs it.

If activated after EIP-8141, pre-fork frame transactions become invalid at the fork boundary and any authorization bound to the pre-fork signature hash MUST be regenerated.

Security Considerations

Replay protection is scoped to (sender, nonce_key, nonce_seq). Different non-zero keys remove only the replay-ordering dependency; transactions on different keys may still conflict on sender or payer balance, contract storage, paymaster state, or any other shared state. This EIP provides replay-domain separation, not confidentiality of key selection: nonce_key is visible in the payload and committed by compute_sig_hash(tx).

Single-use-key applications, such as nullifiers, MUST authenticate at least (sender, nonce_key, nonce_seq == 0) in VERIFY and SHOULD bind the canonical signature hash via TXPARAM(0x08). Treating nonce_seq == 0 alone as authorization is unsafe. Applications deriving nonce_key from a per-use identifier SHOULD domain-separate the input and reject derived keys equal to 0.

A non-zero-key frame transaction does not advance the sender's legacy account nonce during payment approval. If such a transaction executes CREATE at tx.sender, the created address depends on the sender's legacy account nonce at execution time. Another transaction that advances the sender's legacy nonce before inclusion can therefore change the CREATE address without invalidating the keyed transaction. Applications whose semantics depend on a CREATE address SHOULD use CREATE2 or MUST authenticate the expected pre-state legacy nonce via TXPARAM(0x0C).

Nonce consumption persists through later-frame reverts and SENDER atomic-batch rollback because it is part of payment approval. Single-use applications SHOULD minimize post-approval revert paths.

A "send another transaction with the same legacy nonce" cancellation strategy does not invalidate a pending non-zero-key frame transaction. Replacement requires the same (sender, nonce_key, nonce_seq) under the relevant mempool replacement rules, or another transaction that intentionally consumes the same keyed domain.

Each consumed (sender, nonce_key != 0) occupies one persistent slot in NONCE_MANAGER storage; entries are not deleted. State growth is priced by KEYED_NONCE_FIRST_USE_GAS, bounding new keyed slots per block by the block gas limit divided by 20000, before accounting for other transaction costs. nonce_seq == MAX_NONCE_SEQ is reserved as the exhausted state; a key whose current sequence reaches it cannot be advanced further.

Ordinary direct calls to NONCE_MANAGER revert. However, the account may still receive ETH through force-send mechanisms where applicable. Any balance held at NONCE_MANAGER is outside the scope of this EIP and is not recoverable by protocol logic defined here.

Copyright

Copyright and related rights waived via CC0.