Transacting on L2s
Transacting with Keystore Accounts on L2s
Users transact on L2s with keystore-enabled smart accounts in a similar manner as with a standard ERC-4337 smart account, but with some modifications to verify their signing data against the keystore. This is facilitated by the Keystore Validator, which we outlined in the Overview.
Modifying the UserOperation
Signature
The primary difference in userOp structure when transacting with a keystore-enabled smart account lies in the structure of the signature
field within the UserOperation
struct submitted during bundling. The signature is modified to add:
- claimed signing data for the smart account
- an indexed Merkle tree proof of signing data against a keystore state root
Wallets will obtain this information on behalf of users by calling the keystore JSON-RPC API and format it into the userOp signature in the following format:
where proof
and authData
are defined as
The KeyDataMerkleProof
fields represent the following:
bool isExclusion
: Indicates whether the proof is for an exclusion or inclusion IMT proof. Counterfactually initialized accounts will always be exclusion proofs.bytes exclusionExtraData
: Exclusion proofs require some additional fields, which are packed into theexclusionExtraData
field.bytes1 nextDummyByte
: IMT-specific field — whether the next byte is a dummy byte or not.bytes32 nextImtKey
: IMT-specific field — the next key in the IMT.bytes32 vkeyHash
: The hash of thevkey
atkeystoreAddress
.bytes keyData
: Thedata
atkeystoreAddress
.bytes32[] proof
: The Merkle inclusion proof for the IMT node. If this is an exclusion proof, it is an inclusion proof for the previous IMT node.uint256 isLeft
: Bit packed flags indicating whether a node is a left child or right child.
All these fields can be easily queried by a certain keystoreAddress
and block
height with the keystore_getProof
RPC endpoint.
For more information on the IMT and inclusion / exclusion proofs, see the IMT documentation.
Keystore Cost Overhead
Since rollups only have access to a commitment of the keystore state, there is some overhead when transacting on L2 to open this commitment. As such, the primary distinction in transacting via keystore-enabled smart accounts is that signing data must be proven as part of the transaction as opposed to being read from storage. In this section, we analyze this cost overhead of using a keystore-enabled smart account on L2s.
L2 transaction cost analyses must be done across two axes:
- L1 data fee cost, estimated by
384 + 97 * isExclusion + keyData.length + 960
- L2 transaction execution cost
To calculate the overhead of reading from the keystore, we will determine the extra data and execution required to facilitate the keystore read.
Analysis
In terms of calldata usage, the overhead will come from the KeyDataMerkleProof
. Everything else (including the authData
) would be required by a standard smart account as well. For the sake of simplicity, this analysis does not consider compression.
The KeyDataMerkleProof
struct has 8 fields, so we have 8 * 32
= 256
bytes. The struct itself is embedded within a bytes
payload so it has an offset as well, so we have 256 + 32 = 288
bytes. The struct also has 3 dynamic fields, each of which will also have a word dedicated to its length, so we have 288 + 3 * 32 = 384
bytes of fixed calldata. Next, we consider the values of the dynamic fields:
exclusionExtraData
: If this is an exclusion proof, that is an extra97
bytes overhead.keyData
: This is thedata
stored on the keystore and varies with keystore account type. For the m-of-n ECDSA keystore account type, this is97 + amountOfSigners * 32
bytes.proof
: This is the IMT proof. It scales logarithmically with the number of initializedkeystoreAddress
es. If we pessimistically assume that the keystore has1_000_000_000
initializedkeystoreAddress
es, this would yield30 * 32 = 960
bytes.
Thus, the estimated calldata cost is 384 + 97 * isExclusion + keyData.length + 960
.
In terms of execution, the overhead arises from the IMT proof verification. We do not attribute key data consumer execution as overhead since the execution of this logic would take place in a vanilla smart account as well. For a Merkle proof of length 30, the execution cost is ~10k gas, which is quite negligible on L2. The only other dynamic operation is hashing of the keyData
, however, for most reasonably sized keyData
s, this cost should be negligible.
Example
To make the cost analysis more concrete, we consider an example of an exclusion proof of an m-of-n ECDSA keystore account with 5
signers. Let us also assume the IMT tree depth is 30
.
384
bytes of fixed calldata97
bytes ofexclusionExtraData
97 + 5 * 32 = 242
bytes ofkeyData
30 * 32 = 960
bytes ofproof
This yields a total of 384 + 97 + 242 + 960 = 1683
bytes of calldata overhead along with ~10K L2 gas overhead.
Example userOp Signature
In the following example, we will illustrate how to construct the signature
field of the UserOperation
. Picking up from the initialization example, we will use the keystoreAddress
0xe979d22d8a2b069d120b91291d1ea9037b7eeefd1bf8950c331590271ae52578
.
Using this, we can query the keystore_getProof
RPC endpoint to get the data necessary to construct the KeyDataMerkleProof
.
Example output:
Since in this example we are using a counterfactual keystoreAddress
, we will need to construct the exclusionExtraData
field.
We also need to assemble the isLeft
field, which is a bit packed form of the isLeft
flags from the siblings
array.
Let us also get the list of signatures which will serve as the authData
for this m-of-n validation.
Finally, we can construct the signature
field.
And that’s it! Constructing the rest of the UserOperation
remains exactly the same.
In the final section, we will overview how keys on the keystore can be rotated/recovered.