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
Solidity
proof and authData are defined as
Solidity
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 theexclusionExtraDatafield.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 thevkeyatkeystoreAddress.bytes keyData: ThedataatkeystoreAddress.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.
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 the Caching subsection, we outlined that the KV provides a way for users to reuse the opening proofs for a period of time. However, in this section, we analyze this cost overhead in isolation without caching. The cached cost can simply be computed by amortizing the cost of the opening over the amount of transactions which used the cache. 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
Analysis
In terms of calldata usage, the overhead will come from theKeyDataMerkleProof. 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 extra97bytes overhead.keyData: This is thedatastored on the keystore and varies with keystore account type. For the m-of-n ECDSA keystore account type, this is97 + amountOfSigners * 32bytes.proof: This is the IMT proof. It scales logarithmically with the number of initializedkeystoreAddresses. If we pessimistically assume that the keystore has1_000_000_000initializedkeystoreAddresses, this would yield30 * 32 = 960bytes.
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 keyDatas, 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 with5 signers. Let us also assume the IMT tree depth is 30.
384bytes of fixed calldata97bytes ofexclusionExtraData97 + 5 * 32 = 242bytes ofkeyData30 * 32 = 960bytes ofproof
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 thesignature 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.
bash
json
keystoreAddress, we will need to construct the exclusionExtraData field.
Typescript
isLeft field, which is a bit packed form of the isLeft flags from the siblings array.
Typescript
authData for this m-of-n validation.
Typescript
signature field.
Typescript
UserOperation remains exactly the same.
In the final section, we will overview how keys on the keystore can be rotated/recovered.