Stackup Keystore: Scaling Access for Multi-Chain Smart Accounts

Smart accounts offer huge advantages over traditional externally owned accounts (EOAs), with one of them being true flexibility over authentication and authorization.
An EOA can only be authenticated with a secp256k1
signature with no granular controls over account actions. On the other hand, a smart account is not technically restricted in its methods for determining the type of signers and what each signer can do. For instance, we have seen smart accounts authenticated with passkeys or multisig schemes while also implementing granular policies from spending limits to timed sessions.
The current account abstraction ecosystem has many production grade smart account implementations from Safe to Kernel, so why did we build another one?
At Stackup, we've spent the last four years building at every layer of the smart account stack from protocol and infrastructure to application. During this time we continuously ran into three distinct problems that we felt were not adequately solved.
It is a hard fact that accounts today don't just live on one chain but many. For an EOA with static and stateless verification, this is not an issue.
However, smart accounts are complete opposites. Verification can be dynamic and backed by some critical state. This inevitably leads to downstream issues with key rotations.
In the simplest case where a smart account is owned by one public key, this key will be hard coded in the initialization data during its onchain deployment.
Once deployed the owner is free to do a key rotation to a second key for whatever reason. The issue emerges when it becomes time to for the account to be deployed on a second chain.
Since the first public key was hard coded in the initialization data, it directly influences the account's deterministic address. Therefore, to deploy the same account on a new chain, the owner must always maintain access to the first public key. Only after the deployment can they initiate a key rotation to the second key.
This is the unfortunate meta of current smart accounts which has implications in both security and user experience. It breaks best practices in key rotations if a outdated key can still lead to potential loss on a subset of user funds. Additionally, it breaks our expectations that if I rotate my keys then that state should be the same regardless of the network.
Of course, single-owned-accounts are not the only viable setup. Other schemes exist in the real world from N/M multi-sig thresholds or multiple individual signers with varying degrees of authorization.
Although powerful, these setups require state that grows with the number of signers and policies. Let's take the example of an account that can be owned by N
number of users. The account then needs to store N
public keys. This is not a gas friendly approach as storage costs grow linearly with N
.
Also consider that storage is just for a single chain. Assuming a multi-chain account, we then have to replay this update on M
chains that the account is active on.
We could argue that gas cost on L2s are cheap and we only need to optimize on mainnet by restricting the storage size.
While this might be acceptable for some limiting use cases, there is also a burden on the user to sign a transaction for M
chains in order to replay the update.
This is a bad experience that only gets worse as teams operate across more and more chains. We've seen that teams using these kinds of wallets avoid updating configuration across chains, leading to configuration differences and ultimately security vulnerabilities.
Even if gas is not an issue, we should also consider the trade-offs in privacy from storing validation state onchain.
Let's imagine our smart account has a recovery mechanism where we delegate a group of known entities (e.g. friends, family, or services) to assists us in an emergency recovery. For best practice in operational security, it would be highly recommended to not have this information public until the time it is required.
By storing the entire access configuration onchain, we inadvertently give up the option to have such state private by default.
We view these problems as high priority blockers to reaching the full potential of smart accounts, and without it, we will continue to see multi-billion dollar losses due to access control issues.
To solve it, we've built a novel, permissionless, and pragmatic solution in the form of a Keystore contract.
Many of the problems with smart contract wallets are rooted in a tight coupling between the account's code and its configuration. By decoupling the configuration into a Keystore entity that lives independent of the account state, we find that many of the above problems become a lot more manageable.
More specifically, the account delegates validation to the Keystore using an immutable pointer we call a reference hash. Although the pointer cannot change, the value it points to can. Furthermore, this value can change irrespective of whether or not the account code is deployed.
In the following sections, we'll walk through how we solve the issues with account deployment, cross-chain sync, and privacy using the Keystore contract.
Lets get more concrete and circle back to the key rotation and cross chain sync problem but now through the lens of a Keystore.
First we can see that instead of hard coding the public key in the account's initialization data, we now hard code the reference hash which we know can never change. This immediately solves the cross chain deployment problem whereby the user must always have access to the initial key.
The big asterisk here is that this is only true if the configuration that the reference hash points to can be reliably synced across M
chains. Fortunately, this can be much more manageable if the Keystore is designed in a way where updates can be securely created and replayed permissionlessly.
From a user's perspective, this looks like a single signature to create an update action which then gets sent to a delegated relayer for broadcasting across M
chains.
Since configuration is fully decoupled from the account, the delegated relayer can broadcast updates on all chains even if the account is not deployed. By the time a user makes their first transaction on a new chain, they can expect to use their latest key right away.
We are still left with the problem of gas cost and privacy. By picking the right data structure for the pointer value, we can potentially solve both at once.
In the Keystore solution, we use a Merkle Tree data structure to represent the account's configuration. In the protocol this is called the User Configuration Merkle Tree (UCMT).
The leaves of the tree represent a single access rule. For example, each node could be an individual owner with their unique key.
Through several rounds of hashing, we derive a root hash that is unique for a specific combination of nodes. This dynamic root hash is what the immutable reference hash is pointing to onchain.
This data structure has two advantages:
When a specific leaf is required for validation, a Merkle Tree proof can be efficiently verified in O(log N)
space and time complexity where N
is the number of leaves.
Let's now look at a concrete validation flow when an account makes a transaction.
During validation, the account makes a staticcall
to Keystore.validate()
with the following inputs:
The leaf (also called a node
) is a concatenation of a standardized verifier contract address and arbitrary config bytes.
The Keystore will verify the proof to ensure that the node is genuinely apart of the account's configuration. It then does a staticcall
to Verifier.validateData
with the following inputs:
The verifier will attest that a signature is valid and the validation results are returned back up to the account.
The final flow to consider is how a reference hash value is updated. This value always starts as the zero hash, in which case the reference hash itself is used as the value.
When updating the reference hash value, the user (or relayer) makes a call to Keystore.handleUpdates
with the following inputs:
The downstream actions then follow a similar path to validation whereby the proof is validated and a staticcall
to the verifier is made for signature attestation. Assuming all steps are ok, the Keystore will update the reference hash pointed value to the next hash.
A lot of effort has been put into evaluating the Keystore problem space and developing the following solution based on what our team has experienced from working deeply within the account abstraction ecosystem.
We are also excited to launch V1 of the protocol to mainnet with an audit by Spearbit. While this article serves as a primer to the Stackup Keystore protocol, we also encourage users to deep dive into the full specification for a complete picture.
Hazim Jumali is the co-founder and CTO of Stackup, a digital asset management platform streamlining crypto operations for enterprise-grade businesses. With a background in fintech and crypto, he has contributed to open standards in account abstraction and is an Ethereum Foundation grantee for core ERC-4337 infrastructure. At Stackup, he leads engineering and product, shaping secure, scalable onchain tools for modern businesses. Before co-founding Stackup, Hazim studied Electrical Engineering at the University of Melbourne and built consumer-facing products at Afterpay.