Protocol

2D nonces & nonceless

A transaction carries a 2D nonce: a channel key plus a sequence. Sequenced channels give ordered lanes; the reserved NONCE_KEY_MAX channel drops sequencing entirely and leans on expiry plus a fee-invariant replay identifier.

Two fields

Nonce state lives in a precompile at NONCE_MANAGER_ADDRESS; the protocol reads and increments it during processing and exposes a read-only getNonce() to the EVM. Every transaction carries two fields:

ConceptMeaning
nonce_key (uint256)Selects the nonce channel (lane).
nonce_sequence (uint64)Expected sequence number within that channel (must be 0 for nonce-free).
ConceptMeaning
0Standard sequential ordering. Mempool default.
1 … NONCE_KEY_MAX − 1User-defined parallel channels, defined by wallets.
NONCE_KEY_MAXNonce-free. No nonce state read or incremented.

Why parallel lanes

Independent workflows on one account should not queue behind each other. Extra nonce_key values are independent lanes: each channel has its own sequence, so a stuck transaction in one lane never blocks another. Restricted (non-admin) actors need the NONCE grant to use sequenced channels; without it they are limited to NONCE_KEY_MAX (nonceless), which is the default for a fresh session key.

Nonce-free mode

When nonce_key == NONCE_KEY_MAX, the protocol reads and increments no nonce counter and nonce_sequence MUST be 0. Ordering coordination disappears, which is ideal for high-throughput, order-independent workloads like games and bots. Replay protection comes from two things instead:

  • A non-zero expiry: a short wall-clock window the transaction is valid within. Nodes SHOULD reject far-future expiries.
  • A replay identifier: consensus dedup state (a fixed-capacity circular buffer of replay_id → expiry) so the same logical transaction cannot be included twice inside its window.

The replay identifier

A nonce-free transaction has no (nonce_key, nonce_sequence) to key dedup and replacement on, so it carries a replay identifier that names the logical transaction independent of fees and authorization blobs. Standard and 2D transactions do not use it; they dedup and replace on (sender, nonce_key, nonce_sequence).

replay_idtext
REPLAY_ID_TYPE = 0x7901

replay_id = keccak256(REPLAY_ID_TYPE || rlp([
  chain_id, resolved_sender, expiry,
  account_changes, calls, metadata,
  payer
]))

resolved_sender is the recovered address (EOA path) or the sender field (configured-actor path), so two distinct EOAs signing identical bodies still get distinct ids.

Fee bumps vs new logical transactions

This is the key mental model: a transaction can be bumped for gas, but any change to what it does is a new logical transaction.

ConceptMeaning
Excluded from replay_idmax_fee_per_gas, max_priority_fee_per_gas, gas_limit, sender_auth, payer_auth. A fee bump (with a fresh payer_auth when sponsored) is a replacement of the same logical tx.
Included in replay_idchain_id, resolved_sender, expiry, account_changes, calls, metadata, payer. Changing any of these (e.g. retargeting the payer) yields a different id: a new logical tx.

Replacement rules follow from that. A fee bump MUST raise max_priority_fee_per_gas by the node's minimum (e.g. ≥10%) and be independently valid. Sequenced transactions replace on (sender, nonce_key, nonce_sequence); nonce-free transactions replace on (sender, replay_id), and builders MUST NOT include two with the same (sender, replay_id) in one block.

Repeating an action nonce-free

Because the logical transaction is fee-invariant, sending the exact same nonce-free action twice would collide on replay_id. Two ways to do real repeat work:

  • Expiry as entropy: expiry is part of replay_id, so the same action signed with a different (later) expiry is a distinct logical transaction. Repeated actions naturally carry fresh windows.
  • Batching: group the repeats into one transaction's calls (phases) so a single logical transaction does the work, rather than N identical ones.

When you need strict ordering

If an actor genuinely needs an ordered, replay-protected sequence (a high-throughput automation key, say), it opts into a sequenced nonce_key with the NONCE grant and uses the full 2D space. Nonce-free is the default; sequencing is the opt-in.