PerformanceBlog
Tempo MCP serverGive agents search and read tools for Tempo docs
Skip to content
LogoLogo

Account Keychain Precompile

Address: 0xAAAAAAAA00000000000000000000000000000000

Account Keychain overview

The Account Keychain precompile manages authorized Access Keys for accounts, enabling Root Keys (e.g., passkeys) and admin access keys to provision secondary keys. Limited access keys can have expiry timestamps, recurring or one-time per-TIP20 token spending limits, and explicit call scopes that restrict which targets, selectors, and recipients an Access Key may invoke.

authorizeKey(...) takes a KeyRestrictions tuple that bundles expiry, spending limits, and call scopes. authorizeAdminKey(...) provisions an admin key for account key management. Access-key-signed transactions cannot create contracts; use a Root Key for deployment flows.

Motivation

The Tempo Transaction type unlocks a number of new signature schemes, including WebAuthn (Passkeys). However, for an Account using a Passkey as its Root Key, the sender will subsequently be prompted with passkey prompts for every signature request. This can be a poor user experience for highly interactive or multi-step flows. Additionally, users would also see "Sign In" copy in prompts for signing transactions which is confusing. This proposal introduces the concept of the Root Key being able to provision a scoped Access Key that can be used for subsequent transactions, without the need for repetitive end-user prompting. T6 extends this model with admin access keys for key management. Recurring spending budgets and explicit call scoping make limited keys suitable for subscriptions, connected apps, and session-key-style flows.

Concepts

Access Keys

Access Keys are secondary signing keys authorized by an account's Root Key or an admin key. Limited access keys can sign transactions on behalf of the account with the following restrictions:

  • Expiry: Unix timestamp when the key becomes invalid. A non-expiring key_authorization omits expiry at the transaction RLP layer; the protocol translates that omission to u64::MAX before calling the precompile. Direct precompile callers should pass u64::MAX for a non-expiring key. Literal 0 is treated as past expiry and rejected.
  • Spending Limits: Per-TIP20 token limits that deplete as tokens are spent
    • Limits are one-time (period = 0) or recurring (period > 0, in seconds). Recurring limits roll over to max when current_timestamp >= periodEnd.
    • Limits can be updated by the Root Key or an active admin access key via updateSpendingLimit(). An update resets remaining and max to newLimit but preserves the configured period and current periodEnd.
    • Spending limits only apply to TIP20 transfer(), transferWithMemo(), and approve() calls
    • Spending limits only apply when msg.sender == tx.origin (direct EOA calls, not contract calls)
    • Native value transfers and transferFrom() are NOT limited
  • Call Scopes: An Access Key is either unrestricted (allowAnyCalls = true) or restricted to an explicit allowlist of (target, selector, recipients) tuples. An empty allowlist with allowAnyCalls = false means scoped deny-all.
  • No Contract Creation: Access-key-signed transactions cannot perform CREATE or CREATE2, including via factory contracts. Use a Root Key for deployments.
  • Privilege Restrictions: Limited access keys cannot authorize new keys or modify their own limits or scopes. Admin access keys can manage keys, but cannot carry spending limits, call scopes, or expiry.

Authorization Hierarchy

The protocol enforces a strict hierarchy at validation time:

  1. Root Key: The account's main key (derived from the account address)

    • Can call all precompile functions, including the key-management mutators (authorizeKey, authorizeAdminKey, revokeKey, updateSpendingLimit, setAllowedCalls, removeAllowedCalls)
    • Has no spending limits or call-scope restrictions
  2. Admin Access Keys: Secondary authorized keys for account administration

    • Can call key-management mutators, including authorizeKey, authorizeAdminKey, and revokeKey
    • Cannot carry spending limits, call scopes, or expiry
    • Cannot create contracts
  3. Limited Access Keys: Secondary authorized keys for scoped transactions

    • Cannot call mutable precompile functions (only view functions are allowed)
    • Subject to per-TIP20 token spending limits
    • Subject to call-scope checks during execution
    • Cannot create contracts
    • Can have expiry timestamps

Storage

The precompile uses a keyId (address) to uniquely identify each access key for an account.

Storage Mappings:

  • keys[account][keyId] → Packed AuthorizedKey struct (signature type, expiry, enforce_limits, is_revoked, is_admin)
  • spendingLimits[keccak256(account || keyId)][token]SpendingLimitState { remaining, max, period, periodEnd }
  • keyScopes[keccak256(account || keyId)] → Tree of (target, selector, recipients) allowlists used during call-scope checks
  • transactionKey → Transient storage for the key ID that signed the current transaction (slot 0)

AuthorizedKey Storage Layout (packed into single slot):

  • byte 0: signature_type (u8)
  • bytes 1-8: expiry (u64, little-endian)
  • byte 9: enforce_limits (bool)
  • byte 10: is_revoked (bool)
  • byte 11: is_admin (bool)

Interface

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
 
interface IAccountKeychain {
    enum SignatureType {
        Secp256k1,
        P256,
        WebAuthn
    }
 
    struct TokenLimit {
        address token;
        uint256 amount;
        uint64 period;
    }
 
    struct SelectorRule {
        bytes4 selector;
        address[] recipients;
    }
 
    struct CallScope {
        address target;
        SelectorRule[] selectorRules;
    }
 
    struct KeyRestrictions {
        uint64 expiry;
        bool enforceLimits;
        TokenLimit[] limits;
        bool allowAnyCalls;
        CallScope[] allowedCalls;
    }
 
    struct KeyInfo {
        SignatureType signatureType;
        address keyId;
        uint64 expiry;
        bool enforceLimits;
        bool isRevoked;
    }
 
    event KeyAuthorized(address indexed account, address indexed publicKey, uint8 signatureType, uint64 expiry);
    event AdminKeyAuthorized(address indexed account, address indexed publicKey);
    event KeyRevoked(address indexed account, address indexed publicKey);
    event SpendingLimitUpdated(address indexed account, address indexed publicKey, address indexed token, uint256 newLimit);
    event AccessKeySpend(address indexed account, address indexed publicKey, address indexed token, uint256 amount, uint256 remainingLimit);
 
    error UnauthorizedCaller();
    error KeyAlreadyExists();
    error KeyNotFound();
    error KeyExpired();
    error SpendingLimitExceeded();
    error InvalidSpendingLimit();
    error InvalidSignatureType();
    error ZeroPublicKey();
    error ExpiryInPast();
    error KeyAlreadyRevoked();
    error SignatureTypeMismatch(uint8 expected, uint8 actual);
    error CallNotAllowed();
    error InvalidCallScope();
    error InvalidKeyId();
    error LegacyAuthorizeKeySelectorChanged(bytes4 newSelector);
 
    function authorizeKey(
        address keyId,
        SignatureType signatureType,
        KeyRestrictions calldata config
    ) external;
 
    function revokeKey(address keyId) external;
 
    function authorizeAdminKey(
        address keyId,
        SignatureType signatureType,
        bytes32 witness
    ) external;
 
    function updateSpendingLimit(
        address keyId,
        address token,
        uint256 newLimit
    ) external;
 
    function setAllowedCalls(
        address keyId,
        CallScope[] calldata scopes
    ) external;
 
    function removeAllowedCalls(address keyId, address target) external;
 
    function getKey(address account, address keyId) external view returns (KeyInfo memory);
 
    function getRemainingLimitWithPeriod(
        address account,
        address keyId,
        address token
    ) external view returns (uint256 remaining, uint64 periodEnd);
 
    function getAllowedCalls(
        address account,
        address keyId
    ) external view returns (bool isScoped, CallScope[] memory scopes);
 
    function isAdminKey(address account, address keyId) external view returns (bool);
 
    function getTransactionKey() external view returns (address);
}

Behavior

Key Authorization

  • key_authorization includes an optional trailing witness: bytes32 field. When present, the witness is included in the signing hash, checked against the account's burned-witness set, and emitted when the key authorization is registered. The protocol otherwise treats it as opaque and application-defined, so apps can bind a single access-key authorization signature to an offchain challenge.
  • Creates a new key entry with the specified signatureType, config.expiry, config.enforceLimits, and isRevoked set to false
  • If enforceLimits is true, initializes spending limits for each specified token. Each TokenLimit carries a period (0 = one-time, non-zero = recurring in seconds).
  • If allowAnyCalls is false, stores the allowedCalls allowlist in keyScopes. allowAnyCalls = false with allowedCalls = [] means scoped deny-all.
  • Recipient-constrained selector rules are validated before any state is written.
  • Emits KeyAuthorized event

Requirements:

  • MUST be called by the Root Key or an active admin access key
  • MUST be invoked via the canonical selector 0x980a6025 (the (address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[])) shape). Other selectors revert.
  • keyId MUST NOT be address(0) (reverts with ZeroPublicKey)
  • keyId MUST NOT already be authorized with expiry > 0 (reverts with KeyAlreadyExists)
  • keyId MUST NOT have been previously revoked (reverts with KeyAlreadyRevoked - prevents replay attacks)
  • signatureType MUST be 0 (Secp256k1), 1 (P256), or 2 (WebAuthn) (reverts with InvalidSignatureType)
  • config.expiry MUST be strictly greater than the current block timestamp (reverts with ExpiryInPast)
  • To authorize a non-expiring key, omit key_authorization.expiry in the transaction RLP or pass u64::MAX when calling the precompile ABI directly. Do not pass 0.
  • enforceLimits determines whether spending limits are enforced for this key
  • limits are only processed if enforceLimits is true. Duplicate token entries revert with InvalidSpendingLimit.
  • Invalid call-scope shapes (zero targets, duplicate targets, duplicate selectors, duplicate recipients, malformed recipient-bound rules) revert with InvalidCallScope.

Admin Key Authorization

  • Creates a new key entry with is_admin = true.
  • Emits the existing KeyAuthorized event and the additional AdminKeyAuthorized event.
  • Burns the provided witness for replay protection. bytes32(0) is valid.
  • Admin keys can authorize and revoke keys, including other admin keys.
  • Admin keys cannot carry expiry, spending limits, or call scopes.

Requirements:

  • MUST be called by the Root Key or an active admin access key.
  • keyId MUST NOT be the account address (reverts with InvalidKeyId).
  • keyId MUST NOT already be authorized (reverts with KeyAlreadyExists).
  • keyId MUST NOT have been previously revoked (reverts with KeyAlreadyRevoked).
  • signatureType MUST be 0 (Secp256k1), 1 (P256), or 2 (WebAuthn) (reverts with InvalidSignatureType).

Key Revocation

  • Marks the key as revoked by setting isRevoked to true and expiry to 0
  • Once revoked, a keyId can NEVER be re-authorized for this account (prevents replay attacks)
  • Any stored call-scope and periodic-limit state becomes inaccessible. getAllowedCalls(...) returns scoped deny-all (isScoped = true, scopes = []) for revoked keys.
  • Key can no longer be used for transactions
  • Emits KeyRevoked event

Requirements:

  • MUST be called by the Root Key or an active admin access key
  • keyId MUST exist (key with expiry > 0) (reverts with KeyNotFound if not found)

Spending Limit Update

  • Updates the spending limit for a specific token on an authorized key
  • Allows the Root Key or an active admin access key to modify limits without revoking and re-authorizing the key
  • If the key had unlimited spending (enforceLimits == false), enables limits
  • Sets both remaining and max to newLimit. The configured period and current periodEnd are preserved.
  • newLimit MUST fit within TIP20's u128 supply range.
  • Emits SpendingLimitUpdated event

Requirements:

  • MUST be called by the Root Key or an active admin access key
  • keyId MUST exist and not be revoked (reverts with KeyNotFound or KeyAlreadyRevoked)
  • keyId MUST not be expired (reverts with KeyExpired)
  • keyId MUST not be an admin access key, because admin keys do not carry spending limits (reverts with InvalidKeyId)

Allowed Call Updates

  • setAllowedCalls(...) creates or replaces one or more target scopes for an existing key.
  • removeAllowedCalls(...) removes one stored target scope.
  • An empty selectorRules array means any selector on that target is allowed.
  • setAllowedCalls(...) rejects an empty scope batch, zero targets, duplicate targets, duplicate selectors, duplicate recipients, and invalid recipient-constrained rules (reverts with InvalidCallScope).

Requirements:

  • MUST be called by the Root Key or an active admin access key
  • keyId MUST exist and not be revoked
  • keyId MUST not be an admin access key, because admin keys do not carry call scopes (reverts with InvalidKeyId)

View Behavior

  • getKey(...) returns key metadata.
  • getRemainingLimitWithPeriod(...) returns the effective remaining amount and current periodEnd for a key-token pair, accounting for periodic rollover.
  • getAllowedCalls(...) returns (isScoped, scopes). Unrestricted keys return isScoped = false. Scoped keys return isScoped = true with their CallScope[]. Missing, revoked, or expired access keys return isScoped = true, scopes = [] (scoped deny-all).
  • isAdminKey(...) returns true for the Root Key and for active, non-revoked, non-expired admin access keys.
  • getTransactionKey() returns the key used in the current transaction. address(0) means the Root Key.
  • Missing, revoked, or expired keys return zeroed limit values.

Security Considerations

Access Key Storage

Access Keys should be securely stored to prevent unauthorized access. Call scopes make per-app and per-device key isolation more important, because a mis-scoped key may have a broader allowlist than intended.

  • Device and Application Scoping: Access Keys SHOULD be scoped to a specific client device AND application combination. Access Keys SHOULD NOT be shared between devices or applications, even if they belong to the same user.
  • Non-Extractable Keys: Access Keys SHOULD be generated and stored in a non-extractable format to prevent theft. For example, use WebCrypto API with extractable: false when generating Keys in web browsers.
  • Secure Storage: Private Keys MUST never be stored in plaintext. Private Keys SHOULD be encrypted and stored in a secure manner. For web applications, use browser-native secure storage mechanisms like IndexedDB with non-extractable WebCrypto keys rather than storing raw key material.

Privilege Escalation Prevention

Limited access keys cannot escalate their own privileges because:

  1. Management functions (authorizeKey, authorizeAdminKey, revokeKey, updateSpendingLimit, setAllowedCalls, removeAllowedCalls) are restricted to Root Key or active admin-key transactions
  2. The protocol sets transactionKey[account] during transaction validation to indicate which key signed the transaction
  3. These management functions check that the caller is the Root Key or an active admin key before executing
  4. Mutable precompile calls also require msg.sender == tx.origin, which prevents contract-mediated confused-deputy patterns
  5. Limited access keys cannot bypass these checks - transactions will revert with UnauthorizedCaller

Spending Limit Enforcement

  • Spending limits are only enforced if enforceLimits == true for the key
  • Keys with enforceLimits == false have unlimited spending (no limits checked)
  • Spending limits are enforced by the protocol internally calling verify_and_update_spending() during execution
  • Limits are per-TIP20 token and deplete as TIP20 tokens are spent
  • Recurring limits (period > 0) roll over to max when current_timestamp >= periodEnd. Callers can observe rollover state via getRemainingLimitWithPeriod(...).
  • Spending limits only track TIP20 token transfers (via transfer and transferWithMemo) and approvals (via approve)
  • For approvals: only increases in approval amount count against the spending limit. This means approvals indirectly control transferFrom spending, since transferFrom requires a prior approval
  • Non-TIP20 asset movements (ETH, NFTs) are not subject to spending limits
  • Root keys (keyId == address(0)) have no spending limits - the function returns immediately
  • Missing, revoked, or expired keys have an effective remaining limit of zero
  • Failed limit checks revert the entire transaction with SpendingLimitExceeded

Call Scope Enforcement

  • Call-scope checks run on top-level calls signed by an Access Key.
  • If a key is scoped and a call does not match the stored target, selector, and recipient rules, execution reverts with CallNotAllowed.
  • Access-key-signed transactions cannot create contracts in any configuration — including direct CREATE, factory CREATE, and internal CREATE2. Only Root-Key-signed transactions may perform contract creation.

Key Expiry

  • Keys with expiry > 0 are checked against the current timestamp during validation
  • Expired keys cause transaction rejection with KeyExpired error (checked via validate_keychain_authorization())
  • New authorizations require a future expiry timestamp
  • Expiry is checked as: current_timestamp >= expiry (key is expired when current time reaches or exceeds expiry)
  • Expired keys return zeroed limit and call-scope reads.

Usage Patterns

First-Time Access Key Authorization

  1. User signs Passkey prompt → signs over key_authorization for a new Access Key (e.g., WebCrypto P256 key). The signed authorization carries KeyRestrictions, allowing the same first-use flow to provision recurring limits and call scopes.
  2. User's Access Key signs the transaction
  3. Transaction includes the key_authorization AND the Access Key signature
  4. Protocol validates Passkey signature on key_authorization, sets transactionKey[account] = 0, calls AccountKeychain.authorizeKey(), then validates Access Key signature
  5. Transaction executes with Access Key's spending limits enforced via internal verify_and_update_spending(), plus call-scope checks if the key is scoped

Subsequent Access Key Usage

  1. User's Access Key signs the transaction (no key_authorization needed)
  2. Protocol validates the Access Key via validate_keychain_authorization(), sets transactionKey[account] = keyId
  3. Transaction executes with spending limit enforcement via internal verify_and_update_spending() and call-scope enforcement. Contract creation is rejected.

Root Key or Admin Key Revoking or Updating an Access Key

  1. User signs with the Root Key or an active admin key → signs transaction calling revokeKey(keyId), updateSpendingLimit(...), setAllowedCalls(...), or removeAllowedCalls(...)
  2. Transaction executes, marking the Access Key as inactive or updating its restrictions
  3. Future transactions signed by that Access Key are rejected (after revocation) or evaluated against the updated restrictions