Following the recent proposal of a cross-chain Keystore to manage wallet signers—and inspired by Vitalik’s initial blog post on the subject—the Base R&D team embarked on this project, driven by our strong belief in Account Abstraction. In this post, we retrace our journey to develop a practical Keystore implementation and highlight the challenges we encountered along the way.
Initially, when we built the Keystore according to Vitalik's vision, we discovered that relying on ZK technology for this purpose introduced significant downsides. Building, maintaining, and operating a Minimal Keystore (ZK) Rollup proved to be complex—both technically and economically—prompting us to shift to a simpler smart contracts approach. With this second attempt, we realized that using the Keystore to enforce cross-chain signer consistency carries a non-negligible risk of bricking user wallets, mainly because cross-chain messaging is fragile and relies on storage proofs that can break following hard forks.
After extensive exploration, we concluded that the Keystore isn’t yet ready for its originally intended use case: ensuring cross-chain signer consistency (which is most valuable for revoking leaked keys). That said, we still see significant utility in the Keystore for replaying signer states across multiple chains and for enabling advanced wallet features (e.g., time-delayed or ZK email recoveries) more efficiently than current methods.
In this blog post, we share our insights and learnings to help others accelerate their own evaluations and explorations. We remain convinced of the fundamental concept and believe that the ecosystem should work toward its eventual realization.
We begin by explaining why Keyspace is needed, introducing the concept of the Keystore. It then briefly explores viable high-level architectures that we tried and the rationale behind transitioning to a simpler, smart-contract-only, design. Following this, it outlines how to enable Keyspace on existing wallets and the features that it unlocks. It also addresses the current limitations of Keyspace. Finally, the document concludes with a discussion on the project's current state and future potential. Additionally, an appendix on the current state of cross-chain state reading is provided at the end and referenced throughout the post.
Managing smart contract wallets across multiple chains is a growing challenge as the ecosystem becomes increasingly multi-chain. Users often deploy their wallets on several chains—such as Ethereum, Base, or Arbitrum—to take advantage of unique features like lower transaction fees or specific dApps. However, maintaining consistent and secure signer configurations across these networks can quickly become cumbersome and error-prone.
For example, if a user wants to update their wallet’s signer configuration—such as adding a backup signer or transitioning to a multi-signature setup—this typically requires manually updating signer settings on each chain (if it’s even possible). This process is inefficient, introduces potential security risks, and complicates wallet management. Additionally, in the event of a key compromise, revoking or replacing signer keys becomes a time-sensitive and complex task, as it must be performed separately on every chain.
The Keystore concept seeks to solve these challenges by offering a globally accessible and standardized solution for wallet signer management. It provides a single source of truth for signer configurations, enabling seamless cross-chain interoperability while enforcing robust access control mechanisms. With the Keystore, users can update signer settings or respond to security incidents more efficiently and reliably across all connected chains.
With this context in mind, we’ll go in depth into specifics of the Keystore concept, its architecture, and the challenges we encountered during our exploration and development.
The Keystore concept came from the need to ease wallet signers management cross-chain. The idea is simple: expose a unique and globally available Keystore that can be accessed from any chain where a wallet is deployed in order to determine which signer keys control the wallet. This Keystore can generally be thought as a mapping of wallet identifiers to signer configurations:
{
"alice.eth": {config: {signers: ["0xal1ce1", "0xal1ce2"], threshold: 1}},
"bob.eth": {config: {signers: ["0xb0b1", "0xb0b2"]}}
}
The exact structure of the Keystore (and its data) is not precisely as shown above, but the representation remains a helpful way to visualize it.
In the ERC-4337 flow, this Keystore would be accessed when attempting to execute a UserOp. To authorize the execution, the wallet would (somehow) access this globally available Keystore to determine the current signer controlling the wallet and verify that the UserOp is signed by this signer.
The Keystore signer configurations are not fixed; they are initialized with a default value but can be modified. For example, as shown below, alice.eth may wish to add a new signer or adjust her threshold.
{
"alice.eth": {config: {signers: ["0xal1ce1", "0xal1ce2", "0xal1ce3"], threshold: 2}},
"bob.eth": {config: {signers: ["0xb0b1", "0xb0b2"]}}
}
Clearly, bob.eth should not be able to modify alice.eth's signer configuration. Therefore, we need to rethink the Keystore as an authorized mapping of “wallet identifiers” to “signer configurations”, enforcing strict access control rules for updating signer configurations.
{
"alice.eth": {
authorize(sender, newConfig) -> bool: someMultiSigLogic,
config: {
signers: ["0xal1ce1", "0xal1ce2", "0xal1ce3"],
threshold: 2,
}
},
"bob.eth": {
authorize(sender, newConfig) -> bool: someMultiOwnableLogic,
config: {
signers: ["0xb0b1", "0xb0b2"],
}
}
}
It's important to note that, instead of a single authorize logic for the entire Keystore, each identifier can specify its own authorization logic. Although the representation above is somewhat simplified, it effectively illustrates the Keystore’s goal. To reiterate, the Keystore is a unique, globally available, and authorized mapping of wallet identifiers to signer configurations.
Keyspace is our first attempt at implementing a Keystore—a concrete solution that wallets can use today to enhance their cross-chain capabilities. Although the concept may seem straightforward, developing a practical and functional implementation presents its own set of challenges, primarily due to the current limitations in cross-chain state reading.
In a context where wallets are deployed across multiple L2s and alt-L1s, establishing a universally accessible central source of truth is challenging.
Setting aside alt-L1s (which will need to rely on oracle-like solutions, or opt-out of this completely), there are essentially two approaches—both based on the assumption that L2s can, or will eventually be able to, access L1 state*:
Implement the Keystore directly on L1.
Commit only the Keystore state on L1.
both based on the assumption that L2s can, or will eventually be able to, access L1 state*: cross-chain state reading is further explored in the Cross-chain State Reading appendix.
The first approach assumes that the Keystore could take the form of a smart contract, directly deployed on L1, where users would submit transactions to update their signer configuration. Then on the L2s, users would have to provide state proofs of the value of their signer configuration on L1, to be allowed to use their latest signers. In this approach the L1 is used to:
Store the full Keystore state
Run the authorization logic for each Keystore signer update
The downside of this approach is that it requires sending a transaction to L1 each time a wallet’s signer configuration is updated, which, at the time of writing, would cost around $5 (assuming a simple and low-cost authorize logic).
The second approach mitigates the downside of the first by avoiding the need to send a transaction to L1 for every wallet signer configuration update. Instead, a new component would be introduced to aggregate and batch Keystore signer updates, posting only a single commitment of the updated Keystore state to L1. Sounds like a rollup, right? In this scenario, the L1 would be responsible for:
Storing commitments to the Keystore state
Verifying that each new commitment is valid based on the current state
The new component would need to:
Maintain the full Keystore state
Execute the authorize logic for each Keystore signer update
Aggregate and batch Keystore signer updates into commitments for posting on L1
It’s assumed that storing the Keystore state and executing the authorization logic on the new component is vastly more cost-effective than performing the same operations on L1.
The downside of the first approach (implementing the Keystore directly on L1) makes it impractical for wallets. Therefore, the remainder of this article will focus on this second approach.
In the previous section, we explored how committing the Keystore state to L1 enabled efficient cross-chain state sharing (or at least our best attempt at it). However, this approach introduced the need for a new component responsible for processing and applying Keystore signer updates and periodically posting commitments to the Keystore state on L1.This component effectively functions as a rollup, which could be:
A fully independent minimal (ZK) rollup
A general-purpose (EVM) rollup
The first approach involves designing a dedicated minimal Keystore rollup (MKSR) that leverages ZK to batch Keystore signer updates and achieve relatively fast finality*. The entire Keystore state would be stored in an indexed/sparse Merkle Tree, enabling efficient commitment by publishing its root on L1. To ensure correctness when transitioning between commitments, the rollup would also submit a ZK proof (along with the new Keystore state commitment) which would be verified on L1. The different authorize logic would be implemented as ZK circuits and each Keystore identifier (user) would specify which ZK circuit (ECDSA keys, P256 keys, multiownable, multisig etc.) to execute to authorize their update.
relatively fast finality*: Achieving near-instant (<10s) finality is currently hindered by several technical limitations. Specifically, proof generation requires several minutes, and the Keystore state commitment stored on L1 becomes visible to L2s only after a delay of several minutes.
This design was described by Vitalik in this article. Michael de Hoog, Base Principal Software Engineer, presented a technical implementation in this post relying on low level PLONK circuits and eventually, after iterating over it, we switched to using ZK VMs. The switch to ZK VMs was essential to simplify proof aggregation (through unbounded recursion), streamline circuit development (by enabling Rust coding instead of low-level Gnark), and leverage continuous performance and usability enhancements made by contributors in this field daily.
After experimenting with this approach here are the pros and cons we identified:
Pros:
We had full control over the MKSR, allowing us great flexibility to reach our goals.
We achieved relatively fast finality, making Keystore state changes visible to all chains after a few minutes.
This agnostic approach did not take part into specific teams or protocol, favoring a future enshrinement into the EVM.
Cons:
We had to deal with the technical challenges associated with implementing a ZK rollup from scratch.
We faced a tradeoff between achieving fast finality and driving the price per Keystore update down. To mitigate this we envisioned to make the rollup either using a proof aggregator or being an aggregator itself. The goal was to increase the amount of things being proven at once before submitting it to L1.
In order to mitigate the risk of a critical bug in a ZK VM, we had to implement multi-proving and were planning to support SP1, RiscZero and Intel TEE.
The wallet teams willing to use the Keystore needed to split their logic between Rust code (for the authorize logic implemented as a ZK circuit), and Solidity code embedded in their wallet contract (that would read the latest Keystore commitment known, extract the user’s signer configuration and ensure it was used to sign the UserOp being executed).
After testing this approach, we found that the relatively fast finality achieved through ZK didn’t justify the complexity trade-offs and technical limitations it introduced (such as rapid signature expiration). To encourage adoption by smart wallet teams, we needed near-instant finality (<10s).
We concluded that the only way to make Keystore state updates immediately available across chains was to:
Set the Keystore state on relevant chains by replaying state update transactions.
Use cross-chain syncing to synchronize the Keystore state when directly setting is not possible.
From this observation, implementing the Keystore on a general-purpose EVM rollup seemed like a suitable solution. In fact, our second approach builds on insights from the first implementation:
It simplifies Keyspace by utilizing an existing rollup to host the Keystore state and logic.
It allows setting the Keystore state to make updates immediately available on relevant chains.
It implements cross-chain syncing to synchronize the Keystore state when setting is not possible.
In this design, the Keystore consists solely of smart contracts deployed across all supported chains. One chain is designated as the master chain, hosting the master Keystore, while the others function as replica chains, each hosting a replica Keystore.
We began implementing an early version of this approach here. However, this version still had a major issue we faced in the MKSR solution: signatures expiring prematurely due to the master Keystore state being “compressed” into a single commitment (the master chain state root). After digging into commitments and cryptographic accumulators for a few days we came to the conclusion that there is no solution that allows all of the following at the same time:
Commit to a (unbounded) set of elements.
Generate inclusion and exclusion proof.
Given a commitment C1, generate an inclusion proof P1 for an element E1, change the value of another element E2 and generate a new commitment C2, have P1 still verifying against C2.
We addressed this issue by realizing that a globally unique Keystore was no longer necessary. Instead, each user could manage their own Keystore independently and take responsibility for maintaining the consistency of their signer configuration across chains. This enabled us to eliminate the global Keystore with user-specific records and replace it with individual Keystores directly embedded within each wallet (which, as a result, also embeds the authorize logic described in above). The figure below provides a high-level overview of this architecture:
In step 1, Alice updates her Keystore signer configuration to set 0xal1ce as her current signer. Then, in step 1.5, Alice sets her new Keystore state to the relevant chains. Once set, Alice’s wallets on these replica chains can immediately uses her updated Keystore signer configuration to validate UserOps. There are two important points to note:
Both steps 1 and 1.5 run the same authorize logic to ensure that updating Alice’s Keystore signer configuration is allowed based on her current signer configuration.
No consistency is enforced when setting the state*, meaning Alice could set a different signer configuration on a replica chain.
No consistency is enforced when setting the state*: It is important to note that the master Keystore serves as the source of truth only when cross-chain syncing is used. Setting state does not rely on any source of truth.
Step 1.5 could fail because setting state, by default, simply involves replaying the Keystore signer configuration update. For instance, this could fail if the update transaction relies on specific state that is available on some chains but missing on certain replica chains. In such cases, Alice can rely on different cross-chain syncing mechanisms that we implemented (see step 3) to synchronize her Keystore state to the replica chains.
Here is a summary of the pros and cons of this approach:
Pros:
No need to implement, run, and maintain a dedicated minimal ZK rollup.
The Keystore is now fully implemented as Solidity contracts deployed on a single master chain and multiple replica chains. This includes the authorize logic that previously had to be implemented as ZK circuits by wallet teams.
Users can set their Keystore state to instantly use their new signers across chains.
Cons:
Setting state rapidly is important for maintaining wallet security, especially during signer revocations. Any delay in setting state can create vulnerabilities that put wallet funds at risk, for example, if the signer keys are compromised.
Implementing the syncing fallback is complex, as it requires proving storage from the master chain to the replica chains. This complexity is heightened when both chains are L2s (or even L3s), as nested proofs are necessary.
While implementing this approach, we discovered a sneaky yet critical detail (also present in the MKSR implementation): consistency comes with risks. For this reason, consistency (over the Keystore state across chains) was not mentioned in the pros and cons above and will instead be explored in the Keystore consistency section.
However, this new approach provides a better overall wallet user experience (for both users and developers). The remainder of this article will focus on the general-purpose EVM rollup approach and its unique set of challenges.
As of now, integrating the Keystore into an existing wallet involves three steps:
Select a master chain.
Inherit the Keystore contract.
Properly set state.
As explained in above, the Keystore architecture relies on selecting a master chain as the source of truth. This master chain provides the canonical Keystore state, from which replica Keystores can synchronize their state from when needed.
While there are no specific restrictions on which chain can be chosen as the master one, a few key considerations should guide this decision:
The security of the Keystore is tied to the security assumptions of the master chain.
The Keystore’s security also depends on the method used to synchronize state with replica chains. Whenever possible, prioritize “trustless” solutions over oracle-based ones to enhance reliability and security.
It is important to note that, in the current design, the wallet acts as its own Keystore. The next step, therefore, is to extend the wallet contracts by directly inheriting the Keystore contract.
While doing so, the wallet team will need to implement the following specific methods:
_extractConfigHashFromMasterChain: Implements the logic required to extract the signer configuration hash from the master chain, ensuring it can be accessed from any supported replica chain.
_hookIsNewConfigAuthorized: Implements the logic for authorizing new signer configurations. This corresponds to the authorize method described earlier.
_hookApplyNewConfig: Implements the logic to parse and cache the new signer configuration directly in the wallet contract’s storage. This caching improves UX by eliminating the need to provide a master chain storage proof with every wallet transaction or UserOp. Additionally, this hook can be used to execute arbitrary logic, such as performing wallet upgrades when appropriate.
hookIsNewConfigValid: Implements the logic to validate that a new signer configuration is valid. This optional hook helps ensure that the wallet cannot be bricked by unintentionally updating to an invalid signer configuration.
The current repository includes examples for:
Implementing _extractConfigHashFromMasterChain when the master chain is an OPStack chain.
Implementing a simple Keystore signer configuration logic for a MultiOwnableWallet.
Above, we presented a high-level schema demonstrating how to maintain a good wallet UX—comparable to current standards—by allowing users to manually set their Keystore signer configuration to different chains.
In fact, setting state is similar to the replay-transaction mechanism implemented by the Coinbase Smart Wallet team, with one crucial distinction: instead of replaying a transaction, it replays the entire signer configuration (i.e., the Keystore state). This distinction highlights the primary purpose of the Keystore: directly synchronizing the state rather than the transaction history.
On the master chain, no special handling is required as the signer configuration will always reflect the latest state. Updates to the signer configuration originate from transactions sent to the master chain, where the source of truth is maintained.
On replica chains setting the state is left to the discretion of the wallet team. However, a few key considerations can be taken into account:
Revoking a signer: When revoking a signer, the updated state must be immediately set to all replica chains*. Failing to do so can significantly compromise wallet security, as a revoked signer could continue using the wallet, including upgrading it to a malicious implementation, on a replica chain.
Prioritizing active chains: State update can be prioritized for chains that the user frequently interacts with, ensuring an optimal user experience on those chains.
Lazily setting state: For replica chains that the user rarely interacts with, setting state can be deferred until just before they are used. This process can be bundled within a UserOp to minimize overhead.
to all replica chains*: we acknowledge that it may not be practical to replay revocations on all chains, both existing and future. In reality, the wallet provider may only replay revocations on the most relevant chain for the user, which could leave the wallet vulnerable to compromise on other chains where the signer revocation was not applied. The Known issues and Limitations section provides a more detailed explanation of the concept of “weak” signer revocation.
By inheriting the Keystore contract, a wallet exposes all the necessary methods to instantly set its signer configuration or, when necessary, synchronize its state from the master chain.
Vitalik originally proposed two methods for implementing the Keystore:
Light version: Enforcing Keystore state consistency only during signer configuration updates.
Heavy version: Enforcing Keystore state consistency for every transaction or UserOp.
Both approaches aim to enforce state consistency at some point to provide a degree of security guarantees. However, the devil is in the details, and as we will cover in the Keystore consistency section, we believe that enforcing state consistency (regardless of the stage) introduces more drawbacks and fragility to the wallet than actual benefits.
For this reason, our approach intentionally avoids enforcing state consistency and encourages the following:
Manually set state: By setting the Keystore state, each wallet can cache its own keystore signer configuration. This ensures that, in most cases, users only need to send their original transactions without additional steps.
Synchronize state as a fallback: The Keystore also provides the necessary cross-chain messaging primitives to synchronize the user’s signer configuration across chains if setting the state is not possible (i.e., it relies on state that is only available on the master chain).
Bonus: On the master chain, the Keystore is essentially free, as it serves as the source of truth for the user’s signer configuration.
By replicating the entire signer configuration state—instead of relying solely on transaction-based replay—integrating the Keystore into a wallet unlocks advanced features that were previously impossible or required significant effort to implement. Although this approach opens up various use cases, we expect its primary application to be in implementing Recovery Guardians.
It is well known that users can lose access to their keys. In such cases, the ability to trigger a recovery process (similar to those in Web2) is highly desirable. Features such as time-delayed recoveries or, more recently, ZK email recoveries can significantly enhance the user experience during recovery scenarios.
Typically, recovery guardians rely on reading state from the blockchain to authorize recoveries—for example, verifying when a recovery was initiated or checking a DKIM certificate registry to validate ZK email recoveries. In a multi-chain wallet, this would normally require that the necessary data is available on every supported chain. However, with Keyspace, we replay the keystore’s latest full state rather than individual signer changes, allowing the recovery process to be executed on one chain and then bridged to others.
While integrating Keystore into wallets provides cross-chain synchronization of signer configurations and enables the development of new features, it also introduces its own set of challenges.
The use of counterfactual addresses is familiar to anyone using EOAs. You can create a new keypair off-chain, safely share your public key, and start receiving funds at the corresponding address.
Wallets aim to replicate this functionality by leveraging CREATE2 opcode to generate counterfactual addresses. While CREATE2 allows users to know their wallet address in advance and receive funds on chains even if the wallet is not yet deployed, it also introduces significant drawbacks.
Since CREATE2 can only be invoked through smart contracts, it requires the use of a factory contract. To generate the same address, CREATE2 relies on the following:
Dependency on the Same Factory: The same factory contract must be used across all chains indefinitely. Once a factory is used to deploy wallets, it cannot be easily replaced with a new version for existing users. While making the factory upgradeable is an option—since only the factory contract address is relevant—this would require adding permissions to manage the upgrades, which is often undesirable. Moreover, as explained in the next point, upgrading the factory might not even be a practical solution for existing users.
Fixed Contract Bytecode: The deployed contract’s bytecode (the initcode) must be unchanged. The initcode includes both the actual contract deployment code and the constructor parameters. As a result, once a user deploys their wallet to a specific chain, maintaining the same address on new chains requires deploying the exact same implementation with the identical constructor parameters*.
with the identical constructor parameters*: this raises concerns about the risk of bricking the wallet when deploying to new chains, particularly if the user no longer has access to their original signer configuration—possibly due to updates made a long time ago. We expect state set transactions to be replayable (we strongly recommend ensuring replayability), which should significantly reduce the likelihood of such issues. If the state cannot be set, synchronization from the master Keystore should be available as a fallback to unbrick the wallet.
As discussed in the previous section, CREATE2 relies on deploying the same initcode to produce consistent addresses. While this imposes constraints on wallet constructor parameters—such as requiring wallets to always be deployed with the same initial signer configuration—it also introduces complexity into the Keystore implementation.
Since a wallet functions as its own Keystore (inheriting the Keystore contract), the Keystore contract code must remain chain-agnostic to function seamlessly across any chain it is deployed on. This requires addressing two key challenges:
Method Exposure on Different Chains: The Keystore must expose different methods depending on whether it is deployed on the master chain or on replica chains.
Cross-chain State Reading: Regardless of which replica chain the wallet is deployed on, it must be able to read the signer configuration from the master chain so that synchronization can still be used as a fallback when setting state is not possible. The logic for cross-chain state reading heavily depends on the specific chain being read from (i.e., the replica chain). However, implementing separate logic for each replica chain is not feasible, as it would result in different wallet contract bytecodes and, consequently, different deployment addresses.
While the first point is relatively straightforward to handle (e.g., using block.chainid to determine whether the wallet is deployed on the master chain and enabling or disabling relevant methods accordingly), the second one is more nuanced.
As discussed in the Cross-chain State Reading appendix, the ability of replica chains to read from the master chain is closely tied to their ability to access L1 state. However, different replica chains use varying methods to retrieve the L1 state (e.g., obtaining a recent L1 state root).
Our current approach implements various methods that different chains use to access a recent L1 state root. When synchronizing, users can provide a generic L1StateRootProof specifying the logic to execute for retrieving the recent L1 state root. From this L1 state root, the state of the Keystore on the master chain can be extracted.
An example of this approach is the implementation of the _extractConfigHashFromMasterChain method. Internally, it invokes the L1StateRootLib.verify method, which manages the generic retrieval of a recent L1 state root.
Currently, the “generic” logic supports extracting the state root from the L1Block contract on OPStack chains and from the BeaconStateRoots contract for chains that are EIP-4788 compatible.
Additional mechanisms for retrieving recent L1 state roots can be implemented.
While leveraging Keystore wallets provides a simple way to manage signer configurations across chains, one might initially think that Keystore also ensures consistency of wallet signer configurations across all chains. The primary motivation for this assumption would be to enable effective signer revocation across all chains. Although this was indeed a goal of our initial implementation, we realized that it introduces more drawbacks than benefits.
Ideally, we aim for the following:
The master Keystore signer configuration to be immediately visible on all replica chains.
Keystore signer configuration consistency to be enforced at all times –the primary motivation for this would be to have effective signer revocation across all chains.
The first point depends on the finality of the master chain. In the current implementation, consistency could only be enforced “eventually,” with the minimum relevant interval being the time it takes for the master chain to settle on L1.
The second point is more complex. Enforcing eventual consistency introduces significant wallet bricking risks. As discussed in the Cross-chain State Reading appendix, to read the state from the master chain, replica Keystores must implement logic to:
Access the L1 state root: this could be achieved via the L1Block contract on OPStack, by leveraging EIP-4788, or through equivalent solutions.
Verify L1 storage proofs: the Keystore must support verifying L1 storage proofs to confirm the state of the master chain.
Verify the master chain storage proofs: the Keystore must support verifying master chain storage proofs to confirm the state of the master Keystore.
However, either of these steps could fail due to:
Hard forks on the replica chains: a hard fork could deprecate the L1Block contract or equivalent solutions, leaving the Keystore unable to access a recent L1 state root.
L1 storage changes: a switch to Verkle Trees for L1 storage could break Keystore implementations reliant on verifying L1 MPT proofs.
Hard forks on the master chain: a hard fork on the master chain could alter how it settles its state on L1, rendering all replica Keystores unable to verify the master Keystore storage proof.
These scenarios are likely as chains require regular upgrades to evolve and improve. If such changes occur, wallets deployed to replica chains might become stuck. Users would be unable to prove their master Keystore signer configuration without upgrading their wallet implementation to support the new proving logic. However, upgrading is permissioned and guarded by a consistency check, creating a chicken-and-egg problem.
A reliable way to recover from such a scenario would be to use native withdrawal and deposit workflows to propagate the Keystore signer configuration from the master chain to the replica chains. However, this approach requires chain-specific (or ecosystem-specific, such as for the OPStack) implementations, which cannot be easily modified to support new chains after the wallet has been deployed. An existing wallet deployed to a new chain might be unable to synchronize its Keystore configuration from the master chain if the deposit flow was not implemented in the initial version.
For these reasons, we currently believe that enforcing consistency—whether immediate or eventual, and whether for all actions or only for signer configuration updates—is neither worth the complexity it introduces nor the risk of bricking users' wallets.
We acknowledge that our current solution is imperfect, as it does not address the signer revocation issue. However, we believe the ecosystem is not yet ready to effectively solve this problem, as stronger cross-chain messaging primitives that are guaranteed to be hardfork-resistant are still needed.
To clarify, Keyspace currently does not provide signer revocation guarantees across chains. Instead, it offers utility methods that allow users to easily propagate their current Keystore signer configuration across chains, which wallet teams can leverage to implement advanced features.
Our implementation of the Keystore offers nice improvements over existing wallet solutions. However, it still has known issues and limitations that we aim to address in future versions of Keyspace, as cross-chain state reading capabilities continue to evolve.
The current implementation of the Keystore:
is based on a “master-replica” chain architecture.
consists of a single smart contract that wallets can inherit.
allows cross-chain state sharing of wallet signer configurations.
This implementation introduces two key advancements over existing wallet solutions:
Simplified configuration consistency: implementing the Keystore exposes the logic required to maintain the user’s signer configuration consistent across chains.
Support for generic state synchronization: our example implementation demonstrates how the implementation address of the user’s wallet can be stored in their Keystore configuration. Similarly, wallet vendor teams can leverage the Keystore to develop additional features by incorporating custom data into the configuration.
When designing the architecture of the Keystore, we had to balance two key considerations:
Resistance to chain upgrades: ensuring wallets will not get bricked due to future chain upgrades.
Effective cross-chain signer revocation: enabling consistent and reliable signer revocation across all chains (including future ones).
The latter requires enforcing eventual consistency for all actions on the user’s wallet. However, as discussed in the Keystore consistency section, enforcing this comes at a cost, as it poses a non-negligible risk of bricking the user’s wallet in the event of chain upgrades. Currently, the available cross-chain state reading solutions make the two considerations outlined above mutually exclusive*—unless a trusted party is introduced to perform permissionless upgrades of all the user’s wallets.
mutually exclusive*: this limitation can be partially mitigated by relying on native deposits and withdrawals to send cross-chain messages. However, as explained in the Keystore consistency section, this approach does not scale effectively across different L2 stacks.
Since chain upgrades are inevitable and there is no clear timeline for significant improvements in cross-chain state reading, we designed the initial implementation of the Keystore to prioritize resilience to chain upgrades, albeit at the expense of weak signer revocation*.
While this is a genuine issue, it is important to highlight that the exact same issue also affects traditional EOAs (which lack any concept of revocation) as well as current wallet implementations (which currently do not allow revoking signers). Future versions of Keyspace will focus on addressing this issue to provide enhanced cross-chain consistency and strong signer revocations.
weak signer revocation*: "weak" in this context means that revoking a signer does not automatically apply to all existing and future chains. Instead, the revoked signer must be "manually" revoked on all relevant chains.
The long-term goals of Keyspace are to:
Establish a unique source of truth for wallet configurations.
Provide strong guarantees for signer revocation.
Enable near-instant finality, ensuring that wallet configuration changes are immediately reflected across all chains.
Remain cost-efficient and affordable to use.
The path to achieving these goals relies on three major milestones.
As detailed in the Cross-chain State Reading appendix, the ability to share state across chains relies heavily on each chain's capacity to access L1 state. Currently, the ecosystem is fragmented, with different chains supporting various methods for accessing L1 state (e.g., via an L1 block hash or leveraging EIP-4788). Some chains, like Arbitrum at the time of writing, lack support for trustless L1 state access entirely (unless using deposit transactions from L1 which are expensive).
Moreover, all these methods depend on constructing proofs (typically MPT proofs) offchain and submitting them onchain for verification. These proofs are likely to break in the future as L1 evolves its state representation (e.g., transitioning to Verkle Trees).
Addressing these challenges represents a critical first step in improving Keyspace. Achieving this will require the implementation and widespread adoption of new proposals such as L1SLOAD, the more ambitious REMOTESTATICCALL, or equivalent solutions that are resistant to L1 hard forks.
Once the ability to read L1 state from any layer is resolved, the next challenge is reading the state of the master chain from its commitment posted to L1. Similar to reading L1 state, we aim to make this process as robust as possible against potential master chain upgrades that could affect how their state is committed to L1.
Allowing wallet vendor teams to select their own master chains facilitates quick adoption of Keyspace (aligned with a "ship fast" spirit), but it is not an ideal solution. Different master chains use various methods to commit their state to L1 and will evolve and upgrade independently, making the Keyspace ecosystem increasingly complex to manage. As these chains improve and upgrade, the methods for proving their state from their commitments posted to L1 will also diverge, further complicating the system.
The second major step would be to (re)introduce a Minimal Keystore Rollup to serve as the universal master chain for all wallet vendor teams. With this implementation, Keyspace’s resilience would depend solely on upgrades to this single chain (for example, the method of committing its state to L1). This approach simplifies the architecture by transitioning from n replica chains relying on m master chains to n replica chains relying on a single universal master chain, effectively reducing the complexity from an n² problem to an n problem.
There has been considerable hype recently around both based and native rollups—both of which could drastically benefit the MKSR and ease its implementation:
Making the MKSR a based rollup would simplify its sequencing by inheriting L1’s liveness and decentralisation.
Making the MKSR a native rollup would enable efficient verification of Keystore state transitions for batches of updates.
Switching to a universal master chain is also essential for achieving the final milestone, transforming Keyspace into the comprehensive solution we envision.
Finally, once Keyspace relies on a single universal Minimal Keystore Rollup (MKSR), we can envision it being enshrined into the EVM in the future. In this scenario, all nodes would run an MKSR node alongside an L1 node, providing seamless access to the MKSR via an L1SLOAD-equivalent opcode: KEYSTORESLOAD.
This final milestone would usher in a new era of native, user-friendly, multichain wallets by completely eliminating the need to provide MKSR state proofs.
The Keystore, as we envision it in the long term, is an important innovation for enhancing wallet user experiences in an increasingly multichain world. By unifying the management of wallet signers across different chains, it not only streamlines interactions but also improves security through signer revocation—a feature that traditional EOAs simply don’t offer today.
However, for the Keystore to reach its full potential—including enabling signer revocation, which is crucial for mitigating risks when keys are compromised and adds a much-needed layer of protection—a critical challenge must be addressed: the lack of a standardized, robust, and accessible method for retrieving L1 state from L2 networks. Without this cross-chain accessibility, implementing a consistent and secure solution remains out of reach. Moving forward, overcoming this obstacle is essential for realizing the benefits of account abstraction and ensuring that users enjoy a seamless, secure, and unified wallet experience.
As we continue to explore and innovate, collaboration across the ecosystem will be key to bridging these technological gaps. Ultimately, addressing these issues will pave the way for a more resilient and user-friendly multichain future.
When presenting the Keystore architecture, we assumed that L2s can, or will eventually be able to, access L1 state. This capability is essential for syncing a user's Keystore configuration across their wallet contracts deployed on different chains.
This section begins by assuming that L1 state is accessible to all chains, demonstrating how this enables nearly unrestricted crosschain state reading. It then focuses specifically on how L2s (and L3s) can currently access L1 state and explores how this ability might evolve in the future.
For further insights, it is recommended to read Vitalik’s excellent article on cross-L2 reading in the context of wallets.
It is widely known that L2s store their state root in rollup contracts deployed on L1. In the future, L3s will settle on L2s, which in turn will settle on L1. In this ecosystem, the ability to access L1 state from any chain is a key enabler for crosschain state reading.
In the following subsections, we assume that all chains (L2s, L3s etc.) have the ability to directly read L1 state. Let’s explore how state reading would work in this context.
L1-to-L2 state reading is straightforward. Here’s how a storage slot S0 of a contract C_L1 on L1 can be read from a contract deployed on an L2 (C_L2): in C_L2, directly read the L1 state to retrieve the latest value of the S0 slot in the C_L1 contract. That’s it.
However, always requiring direct state updates on L1 to enable crosschain reading is very costly. Consequently, facilitating state reading directly between L2s would be a much more efficient solution.
Let’s build on top of the previous example to see how a storage slot S0 of a contract C_L2_SRC on an L2 (L2_SRC) can be accessed by a contract deployed on another L2 (C_L2_DST). Once again, the ability to directly read L1 state from an L2 is crucial for this process::
In C_L2_DST, perform an L1 storage read to retrieve the latest C_L2_DST state root that was committed in the L2_SRC rollup contract.
In C_L2_DST, accept a storage proof for the S0 slot in the C_L2_SRC contract and verify it against the L2_SRC state root read in 1.
This process is slightly more complex than L1-to-L2 state reading because it involves constructing a storage proof offchain and supplying it to the destination contract on L2. This allows the contract to trustlessly utilize the shared state by verifying it against the retrieved state root using the provided proof.
Building on the example above, this approach appears to be generalizable for reading state across layers of any level.
The foundational observation is that layers of level N+1 post their state root to layers of level N, continuing recursively until reaching level 1 (L1). From this, we can see how L2-to-L3, L3-to-L3, or even L2-to-L5 state reading could be achieved by recursively nesting storage proofs until reaching the target layer.
Let’s see how L5-to-L3 state reading would work:
In C_L3_DST, read the L2_SRC state root posted to L1.
In C_L3_DST, accept a 4-level storage proof and verify it against the L2_SRC state root obtained in step 1. This proof includes the following steps:
Given the L2_SRC state root, extract the L3_SRC state root.
Given the L3_SRC state root, extract the L4_SRC state root.
Given the L4_SRC state root, extract the L5_SRC state root.
Given the L5_SRC state root, extract the storage value at slot S in the contract C_L5_SRC.
This nested proof approach enables trustless state reading across arbitrary layer hierarchies. However, an obvious drawback is that the complexity and size of the proofs grow linearly with the number of layers traversed*. This is because the process always starts from L1 and iterates through the layer hierarchy until reaching the source layer that is reading the state.
the complexity and size of the proofs grow linearly with the number of layers traversed*: while this holds true for naive MPT proofs, this is not entirely accurate as it depends on the type of proof being used
All the previous schemes rely on the ability of L2s (and higher layers) to directly access L1 state, which is crucial for obtaining the source L2 state root needed to construct (potentially nested) storage proofs.
While direct L1 reads are not yet possible—though they may become feasible in the future—L2s currently have the ability to access recent L1 state. This typically involves retrieving a recent commitment to verify a provided L1 state root. Once the L1 state root is verified, any storage slot can be validated by generating a corresponding storage proof.
Currently, most L2s can retrieve recent L1 block hashes, allowing them to prove recent L1 state (by verifying a full L1 block header). However, most of them share the same limitation: older L1 block hashes quickly become inaccessible, making it hard to prove older L1 state.
The adoption of EIP-4788 by some L2s has introduced a new method for accessing L1 state by leveraging Beacon block roots. This approach, without adding significant complexity, slightly extends proof longevity and, with additional effort, can even enable access to any historical L1 state.
Finally, other proposals are being developed to simplify L1 state reading and even enable performing L1 static calls directly from L2s.
OPStack chains provide the L1Block contract, which gives access to information about the most recent known L1 block. According to the documentation, “Values within this contract are updated once per epoch (every L1 block)”.
While having direct access to the latest L1 block from all OPStack L2s is highly convenient, two points are worth noting:
The latest L1 block known to the L2 is usually a few blocks behind the current L1 block.
Since the L1Block contract is updated frequently, directly relying on it for crosschain state reading is unreliable.
The first point introduces some latency, typically consisting of a few L1 blocks. At the time of writing, this latency is approximately ~10 L1 blocks (depending on the chain’s reorg risk tolerance), translating to about a 2-minute delay. While not ideal, this is generally acceptable for most crosschain state reading use cases.
The second point, however, presents a more significant challenge. The generated storage proof is valid for only a single L1 epoch (currently 12 seconds), making it impractical for many scenarios. The schema below illustrates this more clearly.
Fortunately, a quick improvement can be achieved by leveraging the blockhash opcode, which allows the use of older L1 blocks. The logic in the ReadFromC_L1 contract would become:
Do not retrieve the latest known L1 block hash from the L1Block contract.
Accept an arbitrary L2 block header and compute its hash.
Use the blockhash opcode to verify that the provided L2 block header is within the most recent 256 L2 blocks.
Accept a storage proof of the L1Block contract that proves the L1 block hash that was stored and verify it against the L2 state root embedded in the L2 block header provided in step 2.
Accept an arbitrary L1 block header, compute its hash and ensure it matches the hash proven in step 4.
Use the L1 block header (state root) to verify an L1 storage proof.
This slightly more complex approach extends the lifetime of L1 storage proofs from 12 seconds to approximately 8.5 minutes. While this is a significant improvement that enables many use cases, it remains insufficient for scenarios requiring proofs of very old L1 state.
EIP-4788 exposes Beacon chain block roots onchain via a ring buffer of size 8191, providing approximately one day of coverage. While primarily intended to expose Beacon chain information, these block roots can also be used to prove execution state roots (a.k.a. L1 state roots).
The process of proving L1 state is similar to previous approaches, except that the L1 state root—used as the basis for verifying any L1 storage proof—is derived from a recent Beacon block root instead of a recent L1 block hash.
Wilson Cusack implemented a working example of this approach.
This article provides an overview of how Beacon blocks are (serialized and) Merkleized and explains how their root can be used to prove specific fields (i.e. the execution state root).
With slightly more effort, the Beacon block root can also be leveraged to access the Beacon state. Since the Capella fork, this state includes an append-only accumulator of L1 state, historical_summaries, committed every 8192 slots (essentially when the Beacon root ring buffer is full). By generating such proofs, it becomes possible to prove against very old L1 state indefinitely.
All current L1 state reading mechanisms rely on generating L1 storage proofs. While not particularly complex, these proofs create a significant dependency on the structure and commitment method of the L1 state. If this structure were to change (e.g., transitioning to Verkle Trees), all previously generated L1 storage proofs would become invalid.
Future improvements aim to address this challenge by enabling direct reading of L1 state from L2s. This is an active area of development, with several potential approaches being explored:
L1SLOAD precompile (via RIP-7728): This would introduce a basic precompile to load storage slots directly from an L1 contract, eliminating the need to generate and submit L1 MPTproofs onchain.
REMOTESTATICCALL opcode: A more advanced approach that would enable L2 contracts to perform static calls directly to an L1 contract, expanding the functionality and flexibility of L1 state access from L2s.
Both approaches are still in the experimental stage and may not be integrated into the protocol soon due to significant concerns they could introduce.
While new proposals may emerge to improve this process, the simplest and most straightforward way to read L1 state from L3s (and potentially higher layers) today is by using deposit transactions to relay the L1 state root.
This approach was not viable for L1-to-L2 state reading due to high transaction fees on L1. However, it appears to be a practical and efficient option for L2-to-L3 state reading and can be used to propagate the L1 state to L3s. Similarly, if additional layers are introduced in the future, using deposit transactions from the layer directly below seems like a reasonable approach.
We would not be able to research and develop creative systems like Keyspace without the innovations of many teams across the ecosystem - and we'll continue to collaborate as we push the boundaries of what is possible.
If you’re interested in working on creative and ambitious projects to enable the next billion people to come onchain, we’re hiring — and we’d love to hear from you.
Subscribe to this blog and follow us on social to stay up to date with the latest: X (Base team on X) | Farcaster | Discord
Over 200 subscribers