> Feedback: If these docs are stale, missing, or confusing, post sanitized feedback to `https://docs.tempo.xyz/api/feedback` with `source: "mcp"`, a short `message`, and any relevant `toolName`, `relatedResource`, or `client`.
# Account Keychain Precompile

**Address:** `0xAAAAAAAA00000000000000000000000000000000`

## 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

```solidity
// 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.

:::info\[T6 behavior — SDK encoders/decoders]
The [T6 network upgrade](/docs/protocol/upgrades/t6) added **admin access keys** ([TIP-1049](https://tips.sh/1049)). For partners maintaining transaction tooling, the wire-level change is two new trailing optional fields on the `KeyAuthorization` RLP payload:

```text
rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?])
```

SDK encoders/decoders and hardware-wallet firmware that strictly check field count MUST handle the new optionals on T6 networks. The packed `AuthorizedKey` slot also gains an `is_admin` byte at offset 11. The existing `KeyAuthorized` event is **unchanged**; an additional `AdminKeyAuthorized` event is emitted alongside it when `is_admin == true`, so existing indexers continue to work without changes. Provisioning admin keys uses `authorizeAdminKey(keyId, signatureType, witness)`. See the [admin access keys section of the T6 page](/docs/protocol/upgrades/t6#admin-access-keys) for the full surface.
:::

* 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
