Transacting with Keystore Accounts on L2s
UserOperation
Signaturesignature
field within the UserOperation
struct submitted during bundling. The signature is modified to add:
proof
and authData
are defined as
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 the exclusionExtraData
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 the vkey
at keystoreAddress
.bytes keyData
: The data
at keystoreAddress
.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.keystoreAddress
and block
height with the keystore_getProof
RPC endpoint.
For more information on the IMT and inclusion / exclusion proofs, see the IMT documentation.
384 + 97 * isExclusion + keyData.length + 960
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 extra 97
bytes overhead.keyData
: This is the data
stored on the keystore and varies with keystore account type. For the m-of-n ECDSA keystore account type, this is 97 + amountOfSigners * 32
bytes.proof
: This is the IMT proof. It scales logarithmically with the number of initialized keystoreAddress
es. If we pessimistically assume that the keystore has 1_000_000_000
initialized keystoreAddress
es, this would yield 30 * 32 = 960
bytes.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.
5
signers. Let us also assume the IMT tree depth is 30
.
384
bytes of fixed calldata97
bytes of exclusionExtraData
97 + 5 * 32 = 242
bytes of keyData
30 * 32 = 960
bytes of proof
384 + 97 + 242 + 960 = 1683
bytes of calldata overhead along with ~10K L2 gas overhead.
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
.
keystoreAddress
, we will need to construct the exclusionExtraData
field.
isLeft
field, which is a bit packed form of the isLeft
flags from the siblings
array.
authData
for this m-of-n validation.
signature
field.
UserOperation
remains exactly the same.
In the final section, we will overview how keys on the keystore can be rotated/recovered.