Keystore Basics
Inheriting a Keystore
In Keyspace, a wallet's cross-chain keystore is typically embedded within the wallet's smart contract. When you inherit from the Keystore
contract, your wallet gains the ability to sync its configuration across chains.
Since a Keystore
needs know how to read its storage across chains, the logic for verifying cross-chain proofs from your wallet's master chain needs to be provided. The OPStackKeystore
contract shipped with Keyspace provides this logic for OP Stack L2s.
Configuration Hooks
Of the Keystore
's virtual methods that you'll need to implement, the most important are the configuration hooks that are called when the wallet's configuration is changed.
Here's the core logic for configuration updates:
// Hook before (to authorize the new Keystore config).
require(
_hookIsConfigAuthorized({config: config, authorizationProof: authorizeAndValidateProof}),
UnauthorizedNewKeystoreConfig()
);
// Apply the new Keystore config to the internal storage.
bytes32 configHash = applyConfigInternal(config);
// Hook between (to apply the new Keystore config).
bool triggeredUpgrade = _hookApplyConfig({config: config});
// Hook after (to validate the new Keystore config).
bool isNewConfigValid = triggeredUpgrade
? this.hookIsConfigValid({config: config, validationProof: authorizeAndValidateProof})
: hookIsConfigValid({config: config, validationProof: authorizeAndValidateProof});
require(isNewConfigValid, InvalidNewKeystoreConfig());
_hookIsConfigAuthorized
_hookIsConfigAuthorized(ConfigLib.Config calldata config, bytes calldata authorizationProof)
This hook is called before the configuration update is applied. It should verify that the caller is authorized to change the configuration. authorizationProof
is typically a pair of ECDSA signatures for most wallets. Only the first signature is relevant for _hookIsConfigAuthorized
. (The second signature is used for the hookIsConfigValid
.)
If the signature is valid, the hook should return successfully. Otherwise, it should revert.
_hookApplyConfig
_hookApplyConfig(ConfigLib.Config calldata config)
Once a configuration update is authorized, the _hookApplyConfig
is called to apply the update to your wallet's internal storage. There are two typical tasks and one optional task that are performed in this hook:
- Check if the implementation of the wallet needs to be upgraded. If the new configuration stored in the keystore has an implementation address that is different from the address stored in the wallet's storage for its proxy to use, the wallet's implementation should be upgraded.
- Update the wallet's storage with the new configuration. Typically, you'll just decode
config.data
into your locally defined configuration struct and store it. - (Optional) Store any synthesized data from the new configuration. For example, if your wallet uses a mapping of signers, that mapping cannot be serialized into
config.data
as a bytes array. So, you'll need to iterate through the signers and initialize the mapping in the wallet's storage. To get a fresh mapping with each configuration update, you can store this data in its own mapping keyed by the current configuration hash: a new configuration hash will give you a fresh mapping.
hookIsConfigValid (optional)
hookIsConfigValid(ConfigLib.Config calldata config, bytes calldata validationProof)
This hook is called after the configuration update is applied. It's optional. If implemented, it may verify that the update is valid. If the update is invalid, the hook should revert.
A typical implementation will validate a signature of the new configuration hash that is expected to be valid with the new configuration. If the signer of the configuration update is still a valid signer, a separate signature is not needed: just revalidate the same signature that was validated in the _hookIsConfigAuthorized
using the updated configuration.
Otherwise, a second signature is needed, and it can be packed into validationProof
as a pair of signatures. The simplest scenario where a second signature is needed is when a signer is removed from the wallet. This hook would prevent a signer from removing itself without another signature from another signer that proves the new configuration is still usable.
When an implementation upgrade has occurred, hookIsConfigValid
is called via an external call to ensure that the new implementation is executed by the proxy contract for validation.