Abstract
Replaces the single sender nonce of an EIP-8141 frame transaction with (nonce_keys, nonce_seq): a bounded set of nonce keys sharing one sequence number. nonce_keys == [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 whose non-zero key sets do not overlap are replay-independent. This helps privacy applications use one shared sender for many users without forcing every transaction through the same nonce sequence.
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 disjoint non-zero key sets 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 preserves EIP-8141's limit of one pending frame transaction per sender in the public mempool.
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 every selected key to be unused, successful inclusion makes all selected keys 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 | 0x8250968C12e01A19d6F667b9B2F3b3A4d0e51cB7 |
NONCE_MANAGER_CODE | 0x60006000fd |
KEYED_NONCE_FIRST_USE_STATE_GAS | STATE_BYTES_PER_STORAGE_SET * CPSB |
KEYED_NONCE_ACCESS_COST | COLD_STORAGE_ACCESS + STORAGE_WRITE |
MAX_NONCE_SEQ | 2**64 - 1 |
MAX_NONCE_KEYS | 16 |
TXPARAM_LEGACY_NONCE | 0x0D |
TXPARAM_NONCE_KEY_COUNT | 0x0E |
TXPARAM_NONCE_KEYS_HASH | 0x0F |
TXPARAM_NONCE_KEY_0 | 0x10 |
STATE_BYTES_PER_STORAGE_SET and CPSB are defined by EIP-8037. EIP-8037 currently sets STATE_BYTES_PER_STORAGE_SET = 64 and CPSB = 1530, so each fresh key costs 97920 state gas.
COLD_STORAGE_ACCESS and STORAGE_WRITE are defined by EIP-8038. EIP-8038 currently sets COLD_STORAGE_ACCESS = 2100 and STORAGE_WRITE = 10000, so each non-zero key costs 12100 execution gas for the cold read and write of its slot.
NONCE_MANAGER_CODE is a runtime equivalent to revert(0, 0): any ordinary call to NONCE_MANAGER immediately reverts with empty returndata.
NONCE_MANAGER is an ordinary contract deployed by a keyless transaction (see Deployment). It is not installed by the protocol at activation.
Transaction payload
This EIP replaces the nonce field in the EIP-8141 FRAME_TX_TYPE payload with two consecutive fields, nonce_keys followed by nonce_seq. No other payload field is changed. Applied to the EIP-8141 payload, the result is:
[chain_id, nonce_keys, nonce_seq, sender, frames, signatures, fees, blob_versioned_hashes]
fees = [max_priority_fee_per_gas, max_fee_per_gas, max_fee_per_blob_gas]nonce_keys is a list of uint256 values; nonce_seq is a uint64. Each nonce key and nonce_seq MUST be encoded as canonical minimal-length RLP integers. The frame layout and all other EIP-8141 transaction fields are unchanged.
The EIP-8141 signature hash procedure applies after this replacement. The signing payload contains nonce_keys and nonce_seq at the position previously occupied by nonce.
The nonce_keys and nonce_seq encodings are priced as transaction data. Define:
nonce_calldata = rlp(nonce_keys) || rlp(nonce_seq)
nonce_calldata_cost = calldata_cost(nonce_calldata)
nonce_calldata_floor_tokens = floor_tokens_in(nonce_calldata)
keyed_nonce_count = 0 if nonce_keys == [0] else len(nonce_keys)
keyed_nonce_access_cost = keyed_nonce_count * KEYED_NONCE_ACCESS_COSTnonce_calldata is priced exactly as EIP-8141 prices frame and signature data under EIP-7976. Each non-zero key additionally pays for the slot it reads and writes:
- add
nonce_calldata_costandkeyed_nonce_access_costtoframe_tx_intrinsic_gas; - add
nonce_calldata_floor_tokenstocalldata_floor_tokens; - add
keyed_nonce_access_costtocalldata_floor_gas.
nonce_calldata_cost and keyed_nonce_access_cost are execution gas and are derivable from the transaction alone. Every EIP-8141 rule that uses frame_tx_intrinsic_gas, calldata_floor_tokens, or calldata_floor_gas uses these updated quantities; EIP-8141 is otherwise unchanged.
Decoders MUST reject a FRAME_TX_TYPE transaction to which this EIP applies if any of the following is true:
- the payload does not match the EIP-8141 schema after replacing
noncewith consecutive fieldsnonce_keysandnonce_seq; nonce_keysis not an RLP list;len(nonce_keys) < 1orlen(nonce_keys) > MAX_NONCE_KEYS;- any item in
nonce_keys, ornonce_seq, is not encoded as a canonical RLP integer; - any item in
nonce_keysis greater than or equal to2**256; nonce_seq >= 2**64;nonce_keysis not strictly increasing by numeric value;- any item in
nonce_keysis0andnonce_keys != [0].
A FRAME_TX_TYPE transaction to which this EIP applies is invalid if nonce_seq >= MAX_NONCE_SEQ. This bound depends on no state, so clients MAY check it together with the decoding rules above, before stateful validity.
Nonce state
For nonce_keys == [0], the selected nonce domain is the sender's legacy account nonce.
For nonce_keys != [0], each selected nonce key is stored in protocol-managed storage under NONCE_MANAGER.
Let:
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:
for nonce_key in tx.nonce_keys:
assert tx.nonce_seq == current_nonce_seq(tx.sender, 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 whose selected key sets overlap for the same sender are valid in one block only if each transaction's nonce_seq equals the current sequence of every selected key 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_set(sender, nonce_keys, nonce_seq):
if nonce_keys == [0]:
increment_account_nonce(sender)
else:
for nonce_key in nonce_keys:
state[NONCE_MANAGER].storage[slot(sender, nonce_key)] = nonce_seq + 1consume_nonce_set runs exactly once per transaction, on the unique successful payment-scoped APPROVE that sets payer from None to resolved_target.
A payment-scoped APPROVE is an APPROVE whose scope includes APPROVE_PAYMENT, i.e. scope 0x1 or 0x3.
For nonce_keys == [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_keys == [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_keys != [0], transaction validity guarantees tx.nonce_seq < MAX_NONCE_SEQ, so tx.nonce_seq + 1 is in range.
For a payment-scoped APPROVE, clients first perform all EIP-8141 approval checks and execution gas charges, including memory expansion, except the sender existence and state gas checks tied to its nonce increment. The transition below replaces those checks, the state gas charge, and the sender nonce increment.
Only after those checks and costs succeed, and before any approval effect is committed, clients MUST perform:
- If
tx.nonce_keys == [0], setnonce_state_gas = STATE_BYTES_PER_NEW_ACCOUNT * CPSBwhentx.senderdoes not exist under EIP-8037's existence rule immediately before approval. Otherwise set it to0. Iftx.nonce_keys != [0], read each selected slot and setnonce_state_gas = KEYED_NONCE_FIRST_USE_STATE_GAS * first_use_count, wherefirst_use_countis the number of slots whose value is0. - If
state_gas_left < nonce_state_gas, halt the current call frame exceptionally without applying any approval effects. - Charge
nonce_state_gasas state gas fromstate_gas_left. EIP-8141 attributes the charge to the current frame's receipt. - Execute
consume_nonce_set(tx.sender, tx.nonce_keys, tx.nonce_seq). - Commit the remaining EIP-8141 payment approval effects, excluding the sender nonce increment and sender account creation charge replaced above.
Steps 3 through 5 are one approval transition. The state gas charge, nonce consumption, max_cost collection, payer update, and any sender_approved update either all apply or none apply.
If the enclosing EIP-8141 frame reverts or halts exceptionally, clients MUST revert the transition. Once that frame succeeds, a later frame failure, an atomic batch rollback, or a validation-prefix state restore that discards the transaction body MUST NOT revert it unless the transaction is invalid under EIP-8141. Clients MUST discard all effects of an invalid transaction.
The charge counts toward the transaction's and block's state gas totals and is included in fee settlement. It does not consume execution gas or increase gas_used.execution.
Keyed-nonce reads and writes performed by stateful validity and consume_nonce_set 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. They are priced by keyed_nonce_access_cost instead.
Block access list
Keyed-nonce state is ordinary storage of NONCE_MANAGER and is recorded in the EIP-7928 block access list as such. The exemption above only changes gas charging and warming, not block access list recording.
For every transaction with nonce_keys != [0], NONCE_MANAGER MUST appear in the block access list, and each slot written by consume_nonce_set MUST appear in its storage_changes with the transaction's block_access_index and with new_value equal to the value consume_nonce_set writes. A valid transaction writes every slot it reads during stateful validity, and the written value always differs from the value read, so these slots never appear in storage_reads.
Transaction introspection
TXPARAM(0x01) returns tx.nonce_seq. EIP-8141 assigns TXPARAM indices through 0x0C, including 0x0B = len(signatures). This EIP adds four non-conflicting indices:
param | Return value |
|---|---|
0x0D | pre-state legacy sender nonce |
0x0E | len(tx.nonce_keys) |
0x0F | nonce_keys_hash(tx) |
0x10 | tx.nonce_keys[0] |
nonce_keys_hash(tx) is defined as:
keccak256(
uint256_to_bytes32(len(tx.nonce_keys)) ||
concat(uint256_to_bytes32(k) for k in tx.nonce_keys)
)Because valid nonce_keys are strictly increasing, nonce_keys_hash(tx) has one canonical encoding for each selected key set.
TXPARAM(0x0D) 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_keys == [0], stateful validity requires TXPARAM(0x01) == TXPARAM(0x0D) at transaction start. For nonce_keys != [0], they may differ.
Deployment
The nonce manager is deployed like any other smart contract. A special synthetic address is generated by working backwards from the desired deployment transaction:
{
"type": "0x0",
"nonce": "0x0",
"to": null,
"gas": "0x3d090",
"gasPrice": "0xe8d4a51000",
"maxPriorityFeePerGas": null,
"maxFeePerGas": null,
"value": "0x0",
"input": "0x60058060095f395ff360006000fd",
"v": "0x1b",
"r": "0x539",
"s": "0x825000000000000141de",
"hash": "0x5fb73d939ec0b319de03454f0e787a57543d7edcf889a1a8e3fd0473f8fb6c1f"
}Sender: 0x7A57307E9985b0e26B66A7BB920BcaF0396b8198
Address: 0x8250968C12e01A19d6F667b9B2F3b3A4d0e51cB7The init code returns NONCE_MANAGER_CODE. No private key is known for the sender, so no other transaction can deploy code at NONCE_MANAGER.
Activation
This EIP MUST activate at or after EIP-8141.
If timestamp < FORK_TIMESTAMP, clients MUST NOT replace nonce and MUST NOT apply keyed nonce logic.
If timestamp >= FORK_TIMESTAMP, clients MUST replace nonce with nonce_keys and nonce_seq and MUST apply keyed nonce logic.
When this EIP activates, the chain's state MUST already contain NONCE_MANAGER with a nonzero nonce and code NONCE_MANAGER_CODE. A chain meets this requirement by including the deployment transaction in a block before the fork or by including the account in its genesis state with nonce 1. If an already running chain's gas parameters no longer allow the deployment transaction, adding the account requires a coordinated irregular state transition, which is outside the scope of this EIP.
Clients MUST NOT check for NONCE_MANAGER at the fork boundary. Activation changes no state, so it adds nothing to the EIP-7928 block access list.
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.
Mempool
For the EIP-8141 public mempool rules:
- replace the pending transaction identity
(sender, nonce)with(sender, nonce_keys, nonce_seq); - replace every EIP-8141 public mempool dependency on the sender nonce with
current_nonce_seq(sender, nonce_key)for eachnonce_keyinnonce_keys.
If the validation prefix reads TXPARAM(0x0D), the sender's legacy account nonce is an additional dependency.
Nodes MUST revalidate a pending transaction when any of its selected nonce sequences changes. If the validation prefix reads TXPARAM(0x0D), nodes MUST also revalidate when the sender's legacy account nonce changes.
For transactions under this EIP, EIP-8141 Structural Rule 6 also allows a VERIFY frame to use state gas when a payment-scoped APPROVE creates a keyed nonce slot. That state gas draws from the frame's limits.state and does not count toward MAX_VERIFY_GAS. The existing MAX_VERIFY_STATE_GAS cap on the sum of state limits in the validation prefix is unchanged.
All other EIP-8141 public mempool rules, including the limit of one pending frame transaction per sender, remain unchanged.
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_keys == [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_keys are uint256 values 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 explicit fields: 32-byte nonce keys 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.
This EIP intentionally uses one nonce_seq shared by all selected nonce_keys, rather than a vector of (nonce_key, nonce_seq) pairs. A shared sequence preserves the scalar replay semantics of EIP-8141 and keeps transaction introspection simple. Strictly increasing nonce_keys give each selected key set one canonical transaction encoding and one canonical hash. Applications that require single-use capabilities, including privacy nullifiers and expiring authorizations, can derive distinct nonce keys and use nonce_seq == 0. Applications that require atomic advancement of unrelated nonce domains at unrelated sequence numbers are outside the scope of this EIP and may be addressed by a future extension.
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.
NONCE_MANAGER is deployed by an ordinary transaction rather than installed at the fork, as with EIP-7997 and the EIP-8141 expiry verifier. The fork block then makes no irregular state change, so activation adds no block access list entry and needs no rule for an occupied address. Keyed nonce logic is inactive before the fork and ordinary calls to the contract revert, so no pre-fork guard is needed.
Because keyed nonces are ordinary storage, EIP-7928 records them without any change to its format, and a block access list consumer needs no knowledge of this EIP to prefetch, conflict-check, or apply them. A dedicated nonce-change field under the sender would be no smaller for 32-byte keys, would require every consumer to derive the storage slot, and would escape the EIP-7928 item bound, which counts storage keys and addresses but not nonce changes.
The first use of a keyed nonce creates one storage slot, so it pays EIP-8037's state gas for one new slot. Later uses update the same slot and do not pay the creation charge again.
Every use of a key is a cold read and write of one storage slot that adds a 32-byte key and a value to the block access list. KEYED_NONCE_ACCESS_COST is the EIP-8038 access and write components of an SSTORE that changes a cold slot, what a user pays for the same work, and keeps each block access list item above EIP-7928's ITEM_COST. The state-creation component is the existing first-use state gas charge. The cost is a flat intrinsic sum of schedule primitives rather than an EIP-2929 charge because the slots are unreachable from EVM code and the cost must be derivable from the transaction alone; defining it from the primitives lets EIP-8038 repricings flow through, as EIP-2780 does for intrinsic gas. The cost is added to the calldata floor as well as to intrinsic gas, as EIP-8141 does for signature verification. An intrinsic charge alone would be absorbed by a calldata-heavy transaction whose floor already exceeds its standard gas, and its keys would then add 64 block bytes each for no floor cost. In the floor, each key raises the minimum charge by 12100 gas, more than the 4096 gas its block access list bytes would cost at the EIP-7976 floor rate, so no separate byte term is needed.
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 disjoint non-zero key sets, subject to its own dependency-tracking and replacement rules.
Backwards Compatibility
nonce_keys == [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_keys == [0]. Verifier code that previously assumed TXPARAM(0x01) is the legacy account nonce MUST either enforce TXPARAM(0x0E) == 1 and TXPARAM(0x10) == 0 or be updated to handle keyed sequences.
TXPARAM(0x0D) 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_keys, nonce_seq), with validity requiring every selected key to have the selected sequence. Disjoint non-zero key sets remove only the replay-ordering dependency; transactions on disjoint 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_keys are visible in the payload and committed by compute_sig_hash(tx).
Single-use-key applications, such as nullifiers, MUST authenticate at least (sender, nonce_keys_hash, nonce_seq == 0) in VERIFY and SHOULD bind the canonical signature hash via TXPARAM(0x08). Treating nonce_seq == 0 alone as authorization is unsafe. Authenticating only TXPARAM_NONCE_KEY_0 is also unsafe when TXPARAM_NONCE_KEY_COUNT > 1, because an attacker could add unauthorized keys that would be consumed by payment approval. Applications deriving nonce keys from per-use identifiers 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(0x0D).
Nonce consumption persists through later-frame reverts and EIP-8141 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_keys, nonce_seq) under the relevant mempool replacement rules, or another transaction that intentionally consumes an overlapping keyed domain.
Each consumed (sender, nonce_key != 0) occupies one persistent slot in NONCE_MANAGER storage, and entries are never deleted. Creating a slot consumes state gas, so a block can create at most block_gas_limit // KEYED_NONCE_FIRST_USE_STATE_GAS new keyed nonce slots. nonce_seq == MAX_NONCE_SEQ represents an exhausted key; a key at that sequence cannot advance 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.
