UserOperation Nonce

The nonce field on a UserOperation allows for replay protection to ensure the same transaction cannot be executed twice.

The nonce on a User Operation serves the same function as in an EOA transaction. It ensures that the User Operation can only be submitted once. The EntryPoint keeps track of all account nonces and will throw an error if a submitted nonce does not match its record.

2 Dimensional nonces

A User Operation uses a 2 dimensional nonce scheme which is different to a 1 dimensional nonce of an EOA transaction. In an EOA transaction the nonce is a single number that monotonically increases by 1 after each transaction. A User Operation is similar but can have many different monotonic sequences based on a key.

To get the latest nonce for an account, we can use the following userop.js example. This will call the getNonce function directly on the EntryPoint using your given RPC provider.

import { EntryPoint__factory } from "userop/dist/typechain";

const entryPoint = EntryPoint__factory.connect(ENTRYPOINT_ADDRESS, provider);

const nonce = await entryPoint.getNonce(SENDER, key); 

For the majority of simple use cases, we can set the key as 0.

The nonce value

The return value of getNonce will be a unique uint256 number.

function getNonce(address sender, uint192 key) public view override returns (uint256 nonce) {
  return nonceSequenceNumber[sender][key] | (uint256(key) << 64);

Under the hood, the getNonce function has the above implementation. It combines the current key sequence with a left shifted value of the key itself. The end result is to represent all possible 2D nonces as a single number. Let's see some concrete examples.

Given a key of 0, we expect the following nonce values for the given transaction sequence.

Transaction sequenceNonce value

That seems very straightforward. For a key 0, the nonce value is equal to the transaction sequence up to 18,446,744,073,709,551,615. But what if we use a key of 1?

Transaction sequenceNonce value

From this, we can see that transaction sequences for every key is segmented by increments of 18,446,744,073,709,551,615, which is also the max value for a uint64. The maximum possible keys is then equal to the max value for uint192.

Why use 2 dimensional nonces?

It might seem like an overkill to create this 2D abstraction on nonces rather than just a single monotonically increasing number from 0 to max uint256. For the simple use cases, this would be right and we would only care about using the nonce at key equal to 0.

However, if we want to build use cases that require a higher throughput, then 2D nonces are essential. For example, lets say a user wants to make four independent transactions. With only a 1D nonce, a user would have to wait for a transaction to get on-chain before sending the next one to the mempool. Alternatively, they could build each User Operation using nonces from keys 0, 1, 2, and 3. All four transactions can then be submitted at once without blocking each other.