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
nonce
valueThe 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 sequence | Nonce value |
---|---|
0 | 0 |
1 | 1 |
2 | 2 |
18,446,744,073,709,551,615 | 18,446,744,073,709,551,615 |
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 sequence | Nonce value |
---|---|
0 | 18,446,744,073,709,551,616 |
1 | 18,446,744,073,709,551,617 |
2 | 18,446,744,073,709,551,618 |
18,446,744,073,709,551,615 | 36,893,488,147,419,103,231 |
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.
Updated about 1 year ago