Accounts on the Axiom keystore are identified by a new bytes32 keystoreAddress format, which users can counterfactually initialize using the process described in Account Initialization. The state of the Axiom keystore is a mapping

state: mapping(keystoreAddress: bytes32 => (data: bytes, vkey: bytes))

where:

  • bytes data holds the signing data for the keystore account
  • bytes vkey is a verification key for a ZK circuit that ingests data and verifies a ZK authentication proof for the update rule for keystoreAddress which will enable a user to update the (data, vkey) pair.

The keystore state is structured as an Indexed Merkle Tree (IMT), and committed on L1 using the IMT root during each finalization on L1. These state roots can be trustlessly synced to L2s using native message passing or storage proofs, enabling users to prove their signing data against the keystore state root on L2. We use this to create a keystore-enabled smart account transaction flow, which we explain in the rest of this section.

Contract Architecture

For integration with the keystore on L2s, we deploy and maintain the following two contracts on all supported chains:

  • KeystoreStateOracle: The contract which trustlessly caches keystore state roots onto the L2, exposing it to the L2’s execution environment.
  • KeystoreValidator: The contract which facilitates IMT Merkle proofs to verify data against the state root, and validates smart account authentication data against data.

Deployed addresses for these contracts are available in Contract Addresses. We outline an overview of both these contracts below.

Keystore State Oracle

On the L1 bridge contract, provers persist outputRoots into storage during finalization, where an outputRoot is computed as

abi.encodePacked(stateRoot, withdrawalsRoot, lastValidBlockhash)
  • stateRoot is the state root after executing the transactions in the batch.
  • withdrawalsRoot is the withdrawals root after executing the transactions in the batch.
  • lastValidBlockhash is the keystore blockhash of the most recent valid block as of the batch.

The Keystore State Oracle (KSO)‘s role is to read an outputRoot from L1, extract the stateRoot from it, and cache it for use on the L2. The KSO can read outputRoots from L1 in two ways:

  • L1 Merkle Trie Proof: For L2s that expose L1 blockhash access in their execution environments, the KV can read output roots from L1 via a storage proof.
  • Bridge Transaction: For other L2s, the KV can receive output roots from L1 via a native bridge transaction.

Finally, the state root can be extracted by taking a claimed preimage of the outputRoot and checking a hash equivalence.

Axiom is committed to syncing state roots for all supported L2s approximately once per hour. In cases where this sync is delayed or there is a need to expedite the propagation of a finalized state root, a manual sync—whether via storage proof or bridge transaction—can also be triggered permissionlessly.

Keystore Validator

Smart accounts on L2 can become keystore-enabled by using the Keystore Validator (KV) contract. This is an ERC-7579 and ERC-6900 compatible smart account validation module which facilitates IMT Merkle proofs to verify data against state roots cached on the KSO, and validates smart account authentication data against data.

To enable this integration, the KV must be installed as a module within the smart account, which requires compatibility with either the ERC-7579 or ERC-6900 standard. Without compatibility with one of these standards, the smart account cannot support the Keystore Validator.

Deployment details of the KV across all supported L2s are available in Contract Addresses.

The KV expects data to conform to the following structure:

data[0] - domain separator (should be 0x00)
data[1..33] - key data consumer codehash
data[33..] - arbitrary key data

The KV itself will verify a (data, vkey) pair with an IMT proof, but will outsource further authentication against data to an external Key Data Consumer (KDC). There is (generally) a one-to-one correspondence between a vkey and a KDC and data[1..33] allows a vkey to signal how it desires to be authenticated on chain through a CODEHASH which commits to the validation logic of a KDC.

Expiry Checks

When Keystore IMT state roots are persisted into the KSO, they are tagged with an l1BlockTimestamp—the timestamp of the L1 block whose blockhash against which the Merkle trie proof was verified or the bridge transaction was sent. This metadata enables the KV to perform expiry checks, leveraging ERC-4337’s validateUserOp interface, which incorporates native timestamp validation through a return value embedding the validAfter and validUntil fields.

During the installation of the KV, the smart account specifies a stateRootValidityInterval. The validAfter and validUntil fields are then derived as l1BlockTimestamp and l1BlockTimestamp + stateRootValidityInterval, respectively. Given that Axiom guarantees a refresh interval of one hour, it is recommended to set the stateRootValidityInterval to at least 1 hour.

Caching

On L2s, needing to include a Merkle proof within every transaction can be quite expensive. To mitigate this, the KV automatically caches key data reads from the latest state root. This allows smart accounts to optionally read from cached data and skip the Merkle proof, providing users with a configurable tradeoff between L2 transaction cost and data freshness.

To use the cache, during KV installation, the smart account can specify a cacheValidityInterval. If some cached key data that was read from a state root with an l1BlockTimestamp is used, then the validAfter and validUntil fields are derived as l1BlockTimestamp and l1BlockTimestamp + cacheValidityInterval, respectively. Setting the cacheValidityInterval to 0 effectively disallows use of the cache.

When caching is enabled, UIs should clearly display the currently valid set(s) of signing data across all chains. This is crucial as cached values remain valid until either the cacheValidityInterval expires or they are overwritten by newer values. In the case the key rotation is time-sensitive (e.g. due to a compromised key), users should be presented with an option to proactively send cache-overwriting transactions to all relevant chains.

Next Steps

In the upcoming sections, we will explore interacting with the keystore, both directly and from within smart accounts on other rollups, as well as run through an example. However, if you are looking for something specific, you can go there directly:

  • To set up a smart account that reads from a keystore account (without transacting on the keystore itself), check out the guide on Account Initialization.
  • To learn how to construct a userOp bundle for a smart account that interacts with the keystore, including details on its structure and signature formatting, refer to the guide on Transacting on L2s.
  • For information on rotating/recovering keys on the keystore, refer to the guide on Key Rotation and Recovery.