Key rotation is the process of updating the data in the (data, vkey) pair at a keystoreAddress which is done by executing an UPDATE transaction on the keystore. The UPDATE transaction is serialized in the following format:

Solidity
struct KeystoreAccount {
    bytes32 keystoreAddress;
    bytes32 salt;
    bytes32 dataHash;
    bytes vkey;
}

bytes memory transaction = abi.encodePacked(
    KeystoreTxType.UPDATE,
    isL1Initiated,
    l1InitiatedNonce,
    rlp.encode([
        nonce,
        feePerGas,
        newUserData,
        newUserVkey,
        userAcct.keystoreAddress,
        userAcct.salt,
        userAcct.dataHash,
        userAcct.vkey,
        userProof,
        sponsorAcctBytes,
        sponsorProof
    ])
);

Here, the fields are given by:

  • bool isL1Initiated: Whether the transaction is L1-initiated.
  • bytes l1InitiatedNonce: Represents the uint256 L1-initiated nonce for the transaction if the transaction is L1-initiated and the empty bytestring bytes(0x) otherwise.
  • uint256 nonce: The nonce for the transaction.
    • Can be set with the result from the keystore_getTransactionCount RPC endpoint.
  • bytes feePerGas: Represents the uint256 fee per gas for the transaction if the transaction is not L1-initiated and the empty bytestring bytes(0x) otherwise.
    • An estimate for this value can be obtained from the keystore_gasPrice RPC endpoint.
  • bytes newUserData: The new user data to update to.
  • bytes newUserVkey: The new user vkey to update to.
  • KeystoreAccount userAcct: The user account paying for the transaction.
  • bytes userProof: The user’s ZK authentication proof for the transaction.
  • bytes sponsorAcctBytes: Represents the RLP-encoded KeystoreAccount of the sponsor account sponsoring the transaction if the transaction is sponsored and the empty bytestring bytes(0x) otherwise.
  • bytes sponsorProof: Represents the sponsor’s ZK authentication proof sponsoring the transaction if the transaction is sponsored and the empty bytestring bytes(0x) otherwise.

Then, the serialized transaction can be sent to the sequencer via the keystore_sendRawTransaction RPC endpoint.

Cost of a Key Rotation

The cost of a key rotation is the fee for the UPDATE transaction for the specific newUserData and newUserVkey values. Further details on computing this cost can be found in Transaction Fees.

ZK Authentication

Instead of relying on a single signature scheme to authenticate transactions, the keystore uses ZK proofs to allow wrapping of arbitrary authentication logic exposed through a universal interface. The userProof and sponsorProof take (userMsgHash, userDataHash) and (sponsorMsgHash, sponsorDataHash) as public inputs, respectively, where the msgHashes commit to relevant parameters of the transaction which allows verifying that the public key data committed to by dataHashes are consistent with the signers of the msgHashes.

Creating the Message Hash

To maximize compatibility with the existing Ethereum wallet ecosystem, the UPDATE transaction msgHashes conform to the EIP-712 specification. Here are the relevant EIP-712 parameters:

Solidity
bytes EIP712_REVISION = bytes('1');
bytes32 EIP712_DOMAIN =
    keccak256('EIP712Domain(string name,string version,uint256 chainId');
uint256 CHAIN_ID = 999999999;
bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode(EIP712_DOMAIN, keccak256("AxiomKeystore"), keccak256(EIP712_REVISION), CHAIN_ID));

Updates specifically will require the UPDATE_TYPEHASH.

Solidity
bytes32 UPDATE_TYPEHASH =
    keccak256('Update(bytes32 userKeystoreAddress,uint256 nonce,bytes feePerGas,bytes newUserData,bytes newUserVkey)');

Sponsors will also sign an EIP-712 payload using the same parameters as above but with the SPONSOR_TYPEHASH instead.

Solidity
bytes32 SPONSOR_TYPEHASH =
    keccak256('Sponsor(bytes32 sponsorKeystoreAddress,bytes32 userMsgHash,bytes32 userKeystoreAddress)');

msgHashes can be calculated according to the EIP-712 specification.

Solidity
bytes32 msgHash = keccak256(abi.encode(EIP712_DOMAIN, keccak256("AxiomKeystore"), keccak256(EIP712_REVISION), CHAIN_ID));

ZK Authentication Proof Generation

Generating ZK proofs for transaction authentication is currently resource-intensive. As a result, to improve UX and transaction latency, we introduce a new entity into the system: the signature prover. This entity is expected to run robust, high-performance machines capable of generating ZK proofs efficiently to authenticate user transactions on the keystore.

You can read more in Signature Provers.

Send an Update Transaction with the SDK

We provide a Typescript SDK for helping interacting with the keystore. You can find an example of a key rotation using this SDK here.

Latency

The total latency of a key rotation being usable on L2 can be broken down into three phases:

  1. Proof Generation: The total time it takes for the signature prover to generate the ZK proof(s) for a transaction. Even though sponsored transactions have two proofs, since the user and sponsor proofs are generated in parallel, the latency is the same as a non-sponsored transaction. As of the most recent release, the proof generation latency is ~6 min.

  2. Transaction Finalization: The total time from submission of a transaction to the sequencer until finalization on the L1 bridge. This is (most of the time) upper bounded by the prover’s finalization interval which is around once per hour.

  3. L2 Ingestion of L1 State: The time from the mining of an L1 block to an L2 actually becoming aware of the state in that L1 block. In OP Stack rollups, the sequencer can choose how closely the L2 follows the L1 chain, which in the worst case may lead to delays of up to 12 hours. However, for the most part, the L2 typically lags by only about 10–32 blocks, corresponding to an actual delay of around 2–7 minutes.

Combining these three phases, once a user broadcasts their intent to rotate a key, the updated permission set should usually be available for use on L2 in 1-1.5 hours.