EIP-8250 Keyed nonces for frame transactions

Hegota Considered for Inclusion · Account abstraction · EIP status Draft · created 2026-04-16

Replaces the single nonce of a frame transaction with a list of nonce keys and one sequence number. Transactions with non-overlapping keys do not wait for each other's ordering, so privacy apps where many users share one sender address are not blocked.

Why it is needed

Today a frame transaction uses one sequential nonce (a serial number attached so the same transaction is not executed twice) per sender. Because the numbers form a single sequence, if an earlier transaction is delayed, all later transactions from the same sender must wait. This constraint is too heavy for designs in which many users deliberately share one sender address. The main example is privacy protocols. So that on-chain activity is not tied to a specific public address, users send transactions through a single shared sender address. With only one nonce, this address becomes a bottleneck. The moment one user's transaction lands in a block, all pending withdrawals of other, unrelated users become invalid. Smart wallet session keys and relayer-style senders face the same problem. Also, a key that must be used only once, such as a nullifier, needs an atomic record that it has been used, but in the current structure this guarantee can break if a later frame fails.

What changes

Today

After EIP-8250

Who is affected

In depth

Payload: one nonce becomes nonce_keys and nonce_seq

The nonce field in the EIP-8141 FRAME_TX_TYPE payload is replaced by two fields, nonce_keys and nonce_seq. The result is [chain_id, nonce_keys, nonce_seq, sender, frames, signatures, fees, blob_versioned_hashes], and the other fields are unchanged. nonce_keys is a list of uint256 and nonce_seq is a uint64, and both must be encoded as minimal-length RLP integers. The signature hash applies the EIP-8141 procedure as is, with the two fields in place of nonce. The decoder rejects the transaction in these cases: schema mismatch, nonce_keys is not an RLP list, len(nonce_keys) > MAX_NONCE_KEYS (16), an item is not a canonical RLP integer, a key is 2**256 or greater, nonce_seq > MAX_NONCE_SEQ, nonce_keys == [] with nonce_seq == MAX_NONCE_SEQ, or nonce_keys is not strictly increasing by numeric value. The strict increase condition gives each key set a single encoding and hash. Keys are uint256 because privacy protocols derive keys from 32-byte nullifiers or commitments. Unlike ERC-4337, which packs a 24-byte key and an 8-byte sequence into one field, this design holds the key without truncation and keeps the 64-bit sequence width.

NONCE_MANAGER system contract and slot calculation

If nonce_keys == [], the sender's existing account nonce is used. If nonce_keys != [], the sequence number of each key is written directly by the protocol into the storage of NONCE_MANAGER (0x8250968C12e01A19d6F667b9B2F3b3A4d0e51cB7). The slot is slot(sender, nonce_key) = keccak256(left_pad_32(sender) || uint256_to_bytes32(nonce_key)). A missing slot reads as 0 and the protocol never writes 0, so a read value of 0 means the key is being used for the first time. The contract code NONCE_MANAGER_CODE is 0x60006000fd, equivalent to revert(0, 0), so every ordinary call reverts. Only the protocol writes the slots. Contract storage is used instead of extending account state so that keyed nonce state can sit under the existing stateRoot without changing the account MPT structure.

State validity check and single-use keys

The check happens at the same stage as the existing nonce check in EIP-8141, that is, before any frame is executed. If nonce_keys == [], tx.nonce_seq == state[tx.sender].nonce must hold. If nonce_keys != [], the check runs for each selected key. If nonce_seq == MAX_NONCE_SEQ (2**64 - 1), current_nonce_seq(sender, key) == 0 must hold, that is, every key must be unused. Otherwise tx.nonce_seq == current_nonce_seq(sender, key) must hold. MAX_NONCE_SEQ is the single-use form, and consuming it exhausts the key. The check uses the transaction's actual pre-state in block execution order, so two transactions from the same sender with overlapping key sets enter the same block only if each passes all key checks at its own position. The state[tx.sender].nonce read at this check is called tx_legacy_nonce and is exposed through TXPARAM(0x0D).

Nonce consumption tied to payment approval

The step in EIP-8141 where payment approval increments the sender nonce is replaced by consume_nonce_set(sender, nonce_keys, nonce_seq). If nonce_keys == [], increment_account_nonce(sender) raises the current account nonce by 1. It does not overwrite with tx.nonce_seq + 1, so EIP-8141 behavior is preserved even when an earlier frame has already changed the nonce through CREATE, CREATE2, or account deployment. If the incremented value exceeds MAX_NONCE_SEQ, execution aborts with an exception. If nonce_keys != [], MAX_NONCE_SEQ or nonce_seq + 1 is written for each key. This consumption happens once per transaction, at a payment-scope APPROVE (scope 0x1 or 0x3) that changes payer from None to resolved_target. The order is as follows. 1) Compute nonce_state_gas: for an empty key set, STATE_BYTES_PER_NEW_ACCOUNT * CPSB if the sender does not exist; for a key set, KEYED_NONCE_FIRST_USE_STATE_GAS * first_use_count. 2) Abort with an exception if state_gas_left is insufficient. 3) Charge the state gas. 4) Run consume_nonce_set. 5) Commit the remaining approval effects. Steps 3 to 5 are applied together as one transition, all or nothing. If the enclosing frame reverts, the transition is reverted too, but once that frame has succeeded, it is not reverted by a later frame failure or an atomic batch rollback. If the transaction itself is invalid, all effects are discarded.

Gas costs and block access list

The nonce_keys encoding is charged as transaction data. nonce_seq is charged too, except when the key set is non-empty and nonce_seq == MAX_NONCE_SEQ, in which case it is exempt. This keeps the single-use form from costing more than first use of the same key with nonce_seq = 0, and the signature and payload do not change. Each key costs KEYED_NONCE_ACCESS_COST = COLD_STORAGE_ACCESS + STORAGE_WRITE, currently 12100 execution gas. This value and nonce_calldata_cost are added to frame_tx_intrinsic_gas and are also reflected in calldata_floor_tokens and calldata_floor_gas. A key used for the first time creates a new slot, so it costs an additional KEYED_NONCE_FIRST_USE_STATE_GAS = STATE_BYTES_PER_STORAGE_SET * CPSB, currently 97920 state gas. Because these reads and writes are protocol bookkeeping, they are not added to the EIP-2929 accessed set, the EIP-2200 SSTORE pricing is not applied, and they are not made warm for later user access. They are, however, recorded in the EIP-7928 block access list as ordinary storage. For every transaction with nonce_keys != [], NONCE_MANAGER must be in the list, and the slots written by consume_nonce_set appear in storage_changes with new_value. The written value always differs from the value read, so it never appears in storage_reads.

TXPARAM extension and compatibility

TXPARAM(0x01) returns tx.nonce_seq. Four new indexes are added. 0x0D is the pre-state account nonce tx_legacy_nonce, 0x0E is len(tx.nonce_keys), 0x0F is nonce_keys_hash(tx), and 0x10 is tx.nonce_keys[0] (0 if there are no keys). nonce_keys_hash is keccak256(uint256_to_bytes32(len) || the 32-byte concatenation of each key), and because keys are strictly increasing, each key set has exactly one hash. 0x0D is a transaction-scope value, so it is not changed by payment approval, key consumption, account deployment, CREATE, or CREATE2. If nonce_keys == [], TXPARAM(0x01) == TXPARAM(0x0D) at the start, but they can differ once a key is used. Therefore verification code that assumed TXPARAM(0x01) is the account nonce must enforce TXPARAM(0x0E) == 0 or be updated to handle keyed sequence numbers. Existing account objects, non-frame transaction types, and eth_getTransactionCount are unchanged.

Deployment, activation, and mempool rules

NONCE_MANAGER is not planted by the protocol at the fork but deployed through a keyless deployment transaction. The transaction has type 0x0, nonce 0x0, gas 0x3d090, gasPrice 0xe8d4a51000, input 0x60058060095f395ff360006000fd, v 0x1b, r 0x539, s 0x825000000000000141de, and its sender is 0x7A57307E9985b0e26B66A7BB920BcaF0396b8198. The private key of this sender is not known, so no other transaction can place code at the same address. Activation must be at or after EIP-8141, and at activation NONCE_MANAGER must already have a non-zero nonce and NONCE_MANAGER_CODE. Either include the deployment transaction in a pre-fork block or put the account in genesis with nonce 1. Clients do not check this account at the fork boundary, and activation itself does not change state. Pre-fork frame transactions and the authority bound to their signature hash do not carry across the boundary, so they must be evicted from the mempool and rebuilt. In the mempool, the pending transaction identifier changes to (sender, nonce_keys, nonce_seq), and the sender nonce dependency changes to current_nonce_seq for each selected key. If the validation prefix reads TXPARAM(0x0D), the account nonce is also part of the dependency. A change in the selected sequence number requires revalidation. When a payment-scope APPROVE creates a new slot, the VERIFY frame can spend state gas from limits.state, and this does not count toward MAX_VERIFY_GAS. The limit of one pending frame transaction per sender is unchanged.

Security considerations and current status

The replay protection scope is (sender, nonce_keys, nonce_seq). Even when keys do not overlap, conflicts can still arise in sender and payer balances, contract storage, and paymaster state, and because nonce_keys is exposed in the payload, key selection is not secret. A single-use app such as a nullifier scheme must authenticate at least (sender, nonce_keys_hash, nonce_seq == MAX_NONCE_SEQ) in VERIFY, and it is best to bind the canonical signature hash with TXPARAM(0x08). Checking only nonce_seq == MAX_NONCE_SEQ or only TXPARAM_NONCE_KEY_0 is unsafe, because an attacker can append keys and have them consumed at payment approval. Keyed transactions do not increment the account nonce, so a CREATE from tx.sender may produce a different address; use CREATE2 or authenticate the expected nonce with TXPARAM(0x0D). The cancellation method of sending a different transaction with the same account nonce cannot invalidate a keyed transaction. A consumed slot is never deleted and remains permanently, and the number of new slots per block cannot exceed block_gas_limit // KEYED_NONCE_FIRST_USE_STATE_GAS. This EIP is in Draft status, so its content may change. In the call records, it was received as a Hegotá candidate (PFI) at 07.02 ACDE #240, moved up to Considered for Inclusion (CFI) for Hegotá at 09.24 ACDE #246, and was confirmed for inclusion in bogota-devnet-1 together with EIP-8141, EIP-8272, and EIP-7906 at 10.12 ACDT #100.

Original EIP · ← EIPs