---
eip: 8250
title: Keyed Nonces for Frame Transactions
description: Independent nonce domains for frame transactions
author: Thomas Thiery (@soispoke), Toni Wahrstätter (@nerolation), lightclient (@lightclient), Vitalik Buterin (@vbuterin)
discussions-to: https://ethereum-magicians.org/t/eip-8250-keyed-nonces-for-frame-transactions/28437
status: Draft
type: Standards Track
category: Core
created: 2026-04-16
requires: 8141
---

## Abstract

Replaces the single sender nonce of an [EIP-8141](./eip-8141.md) 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](./eip-8141.md).

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

```text
[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:

```text
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`.

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

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

```python
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](./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.

### 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](./eip-8141.md).

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](./eip-4337.md) 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](../LICENSE.md).
