Updating Your Keystore
Master and Replica Chains
In Keyspace, a wallet's keystore is built into the wallet contract, and the wallet itself is deployed to multiple chains. The wallet vendor chooses one of these chains to be the master chain, the single source of truth for the wallet's configuration. This is typically a general purpose L2 chain with low fees. Every other chain that the wallet is deployed to is a replica chain.
Keyspace currently ships with support for any OP Stack L2 to be used as a master chain. As of v0.1.0, the replica chains where syncing is supported are OP Stack L2s and any L2 that implements EIP-4788's beacon root oracle. The syncing methods we aim to support are described below.
Building Your Next Configuration
Configurations in Keyspace are defined by the account
address for the keystore, the data
stored in the keystore, and the nonce
of the configuration. To make a change to a wallet's configuration, you first need to build the next data.
The format of the data
byte string is defined by the wallet vendor. The most important consideration during an update is that the desired mutation is applied correctly to the configuration without accidentally changing or discarding any other parts of the configuration.
Once you have the new data, you need the next nonce for the configuration, which is the current nonce plus one. keyspace-client
's buildNextConfig
will fetch the next nonce for you. It also takes the previous configuration data that you applied your mutations to and checks it against the configuration hash stored onchain to make sure you're not applying your changes to an outdated or incorrect configuration.
To get the configuration hash, call hashConfig
with the new configuration returned by buildNextConfig
. That hash needs to be signed to produce the authorizationProof
needed to make a configuration change, which is consumed by your wallet's hooks (see Keystore Basics).
Changing the Configuration on the Master Chain
function setConfig(ConfigLib.Config calldata newConfig, bytes calldata authorizationProof)
Keystore.setConfig
is the function that makes a configuration change on the master chain. It takes the full new configuration struct and the authorizationProof
produced by signing the configuration hash with the wallet's private key.
Syncing to Replica Chains
Keyspace helps you keep your wallet's configuration in sync across different chains. Syncing is optional, and is mainly recommended for Ethereum rollups. Even on chains where syncing is disabled, Keyspace's state-based configuration management helps you develop features that can replay their configuration changes on other chains without any feature-specific syncing work.
function confirmConfig(ConfigLib.Config calldata newConfirmedConfig, bytes calldata keystoreProof)
Keystore.confirmConfig
is the function that confirms a configuration change on a replica chain. It takes the full new configuration struct and a keystoreProof
with data to prove the configuration hash from the master chain. There are several methods for proving the configuration hash, and the keystoreProof
will be different depending on the method.
Syncing via Merkle Proofs
Cross-chain Merkle proofs are the most efficient syncing method, as they only have gas costs on the replica chain the user wants to use their wallet on, which is typically a low-cost L2. They're also the most fragile method: each hard fork of an L1 or L2 can change the assumptions that the Merkle proof relies on, and would require a contract upgrade with new logic to verify cross-chain state.
keyspace-client
's getMasterKeystoreProofs
retrieves the proofs needed to confirm a configuration change on a replica chain for an OP Stack L2 master chain.
Proving the L1 State Root
Rollups typically have some way to access the state of the L1 chain. OP Stack rollups have two methods: the hash
storage slot of the L1Block
predeploy and the EIP-4788 beacon root oracle, which can be used to prove the execution state root of a given block. We currently expect rollups to standardize on EIP-4788 as the method to access L1 state because its ring buffer design produces longer-lived proofs, and the beacon chain itself includes a double-batched accumulator that makes proofs of any L1 state since the merge much more efficient.
When using the L1Block
predeploy, proofs rooted at L1Block.hash
are only valid for one L1 block time (12 seconds). For longer-lived proofs, we prove the storage slot for L1Block.hash
and use the BLOCKHASH
opcode to provide the root for the proof, which lasts for 256 replica chain blocks.
Proving the L1 State Root on Alt-L1s
On alt-L1 chains, there's no trustless way to access the state of the L1 chain. Wallets can either disable syncing on these chains or rely on an oracle to provide the state root. Future releases of Keyspace will use Hashi to require multiple trusted oracles to agree on an L1 block root, then prove the state root from there.
Proving the Master Chain State Root
Proving the master chain state root typically requires an L1 state proof of the master chain's bridge contract(s). Currently, Keyspace only supports the OP Stack L2s as the master chain via the OPStackKeystore
contract, which implements Keystore
's abstract _extractConfigHashFromMasterChain
method. Support for other L2s can be implemented by following the same pattern.
For OP Stack L2s, we prove the anchors(0)
slot of the AnchorStateRegistry
contract to prove the latest master chain output root. The state root is part of the preimage of the output root.
Proving the Keystore Configuration Hash
Once we have the state root for the master chain, we just need to prove storage slots within the wallet contract on that chain. The Keystore
contract defines its own storage offset for this data, and the configuration hash is stored at that offset.
Syncing via Deposits and Withdrawals
Deposits and withdrawals are the canonical method for sending messages between chains. That makes them extemely resilient: the whole ecosystem builds on top of withdrawals and deposits with the expectaction that they will succeed for the lifetime of the chains. Rollup teams ensure that their bridge contracts continue to function through each hard fork.
The downsides of deposits and withdrawals are that they require a transaction to be sent on a separate chain from the one the user is interacting with, and that these transactions have significant costs on L1.
We expect deposits and withdrawals to mainly be used as method of last resort for syncing your wallet's configuration when the replica chain doesn't have a source of L1 state roots, or when a hard fork has broken the Merkle proof syncing method.
Withdrawing to L1
Your wallet's configuration can already be deposited to any L3 built on top of the master chain, but not anywhere else. To get your configuration to other chains, the first step is to get it to L1. The withdrawal flow requires one transaction on the master chain, then two transactions on L1. It also requires waiting for the challenge period to end before the configuration can be withdrawn to L1.
Depositing to Rollups
Once your configuration is on L1, you can deposit it to any L2 using their native deposit method. This requires one more L1 transaction and a wait of a few minutes. This method can also be used to sync from an L2 to one of its L3 chains.