Account Keychain Precompile
Address: 0xAAAAAAAA00000000000000000000000000000000
Overview
The Account Keychain precompile manages authorized Access Keys for accounts, enabling Root Keys (e.g., passkeys) to provision scoped "secondary" Access Keys with expiry timestamps and per-TIP20 token spending limits.
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.
Concepts
Access Keys
Access Keys are secondary signing keys authorized by an account's Root Key. They can sign transactions on behalf of the account with the following restrictions:
- Expiry: Unix timestamp when the key becomes invalid (0 = never expires)
- Spending Limits: Per-TIP20 token limits that deplete as tokens are spent
- Limits deplete as tokens are spent and can be updated by the Root Key via
updateSpendingLimit() - Spending limits only apply to TIP20 token transfers, not ETH or other assets
- Limits deplete as tokens are spent and can be updated by the Root Key via
- Privilege Restrictions: Cannot authorize new keys or modify their own limits
Authorization Hierarchy
The protocol enforces a strict hierarchy at validation time:
-
Root Key: The account's main key (derived from the account address)
- Can call all precompile functions
- Has no spending limits
-
Access Keys: Secondary authorized keys
- Cannot call mutable precompile functions (only view functions are allowed)
- Subject to per-TIP20 token spending limits
- Can have expiry timestamps
Storage
The precompile uses a keyId (address) to uniquely identify each access key for an account.
keys[account][keyId]→ PackedAuthorizedKeystruct (signature type, expiry, enforce_limits, is_revoked)spendingLimits[keccak256(account || keyId)][token]→ Remaining spending amount for a specific token (uint256)transactionKey→ Transient storage for the key ID that signed the current transaction (slot 0)
- byte 0: signature_type (u8)
- bytes 1-8: expiry (u64, little-endian)
- byte 9: enforce_limits (bool)
- byte 10: is_revoked (bool)
Interface
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
interface IAccountKeychain {
/*//////////////////////////////////////////////////////////////
STRUCTS
//////////////////////////////////////////////////////////////*/
/// @notice Signature type
enum SignatureType {
Secp256k1,
P256,
WebAuthn,
}
/// @notice Token spending limit structure
struct TokenLimit {
address token; // TIP20 token address
uint256 amount; // Spending limit amount
}
/// @notice Key information structure
struct KeyInfo {
SignatureType signatureType; // Signature type of the key
address keyId; // The key identifier
uint64 expiry; // Unix timestamp when key expires (0 = never)
bool enforceLimits; // Whether spending limits are enforced for this key
bool isRevoked; // Whether this key has been revoked
}
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
/// @notice Emitted when a new key is authorized
event KeyAuthorized(
address indexed account,
bytes32 indexed publicKey,
uint8 signatureType,
uint64 expiry
);
/// @notice Emitted when a key is revoked
event KeyRevoked(address indexed account, bytes32 indexed publicKey);
/// @notice Emitted when a spending limit is updated
event SpendingLimitUpdated(
address indexed account,
bytes32 indexed publicKey,
address indexed token,
uint256 newLimit
);
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error KeyAlreadyExists();
error KeyNotFound();
error KeyInactive();
error KeyExpired();
error KeyAlreadyRevoked();
error SpendingLimitExceeded();
error InvalidSignatureType();
error ZeroPublicKey();
error UnauthorizedCaller();
/*//////////////////////////////////////////////////////////////
MANAGEMENT FUNCTIONS
//////////////////////////////////////////////////////////////*/
/**
* @notice Authorize a new key for the caller's account
* @dev MUST only be called in transactions signed by the Root Key
* The protocol enforces this restriction by checking transactionKey[msg.sender]
* @param keyId The key identifier (address) to authorize
* @param signatureType Signature type of the key (0: Secp256k1, 1: P256, 2: WebAuthn)
* @param expiry Unix timestamp when key expires (0 = never expires)
* @param enforceLimits Whether to enforce spending limits for this key
* @param limits Initial spending limits for tokens (only used if enforceLimits is true)
*/
function authorizeKey(
address keyId,
SignatureType signatureType,
uint64 expiry,
bool enforceLimits,
TokenLimit[] calldata limits
) external;
/**
* @notice Revoke an authorized key
* @dev MUST only be called in transactions signed by the Root Key
* The protocol enforces this restriction by checking transactionKey[msg.sender]
* @param keyId The key ID to revoke
*/
function revokeKey(address keyId) external;
/**
* @notice Update spending limit for a specific token on an authorized key
* @dev MUST only be called in transactions signed by the Root Key
* The protocol enforces this restriction by checking transactionKey[msg.sender]
* @param keyId The key ID to update
* @param token The token address
* @param newLimit The new spending limit
*/
function updateSpendingLimit(
address keyId,
address token,
uint256 newLimit
) external;
/*//////////////////////////////////////////////////////////////
VIEW FUNCTIONS
//////////////////////////////////////////////////////////////*/
/**
* @notice Get key information
* @param account The account address
* @param keyId The key ID
* @return Key information (returns default values if key doesn't exist)
*/
function getKey(
address account,
address keyId
) external view returns (KeyInfo memory);
/**
* @notice Get remaining spending limit for a key-token pair
* @param account The account address
* @param keyId The key ID
* @param token The token address
* @return Remaining spending amount
*/
function getRemainingLimit(
address account,
address keyId,
address token
) external view returns (uint256);
/**
* @notice Get the transaction key used in the current transaction
* @dev Returns Address::ZERO if the Root Key is being used
* @return The key ID that signed the transaction
*/
function getTransactionKey() external view returns (address);
}Behavior
Key Authorization
- Creates a new key entry with the specified
signatureType,expiry,enforceLimits, andisRevokedset tofalse - If
enforceLimitsistrue, initializes spending limits for each specified token - Emits
KeyAuthorizedevent
- MUST be called by Root Key only (verified by checking
transactionKey[msg.sender] == 0) keyIdMUST NOT beaddress(0)(reverts withZeroPublicKey)keyIdMUST NOT already be authorized withexpiry > 0(reverts withKeyAlreadyExists)keyIdMUST NOT have been previously revoked (reverts withKeyAlreadyRevoked- prevents replay attacks)signatureTypeMUST be0(Secp256k1),1(P256), or2(WebAuthn) (reverts withInvalidSignatureType)expiryCAN be any value (0 means never expires, stored as-is)enforceLimitsdetermines whether spending limits are enforced for this keylimitsare only processed ifenforceLimitsistrue
Key Revocation
- Marks the key as revoked by setting
isRevokedtotrueandexpiryto0 - Once revoked, a
keyIdcan NEVER be re-authorized for this account (prevents replay attacks) - Key can no longer be used for transactions
- Emits
KeyRevokedevent
- MUST be called by Root Key only (verified by checking
transactionKey[msg.sender] == 0) keyIdMUST exist (key withexpiry > 0) (reverts withKeyNotFoundif not found)
Spending Limit Update
- Updates the spending limit for a specific token on an authorized key
- Allows Root Key to modify limits without revoking and re-authorizing the key
- If the key had unlimited spending (
enforceLimits == false), enables limits - Sets the new remaining limit to
newLimit - Emits
SpendingLimitUpdatedevent
- MUST be called by Root Key only (verified by checking
transactionKey[msg.sender] == 0) keyIdMUST exist and not be revoked (reverts withKeyNotFoundorKeyAlreadyRevoked)keyIdMUST not be expired (reverts withKeyExpired)
Security Considerations
Access Key Storage
Access Keys should be securely stored to prevent unauthorized access:
- 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: falsewhen 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
Access Keys cannot escalate their own privileges because:
- Management functions (
authorizeKey,revokeKey,updateSpendingLimit) are restricted to Root Key transactions - The protocol sets
transactionKey[account]during transaction validation to indicate which key signed the transaction - These management functions check that
transactionKey[msg.sender] == 0(Root Key) before executing - Access Keys cannot bypass this check - transactions will revert with
UnauthorizedCaller
Spending Limit Enforcement
- Spending limits are only enforced if
enforceLimits == truefor the key - Keys with
enforceLimits == falsehave 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
- Spending limits only track TIP20 token transfers (via
transferandtransferFrom) and approvals - For approvals: only increases in approval amount count against the spending limit
- 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 - Failed limit checks revert the entire transaction with
SpendingLimitExceeded
Key Expiry
- Keys with
expiry > 0are checked against the current timestamp during validation - Expired keys cause transaction rejection with
KeyExpirederror (checked viavalidate_keychain_authorization()) expiry == 0means the key never expires- Expiry is checked as:
current_timestamp >= expiry(key is expired when current time reaches or exceeds expiry)
Usage Patterns
First-Time Access Key Authorization
- User signs Passkey prompt → signs over
key_authorizationfor a new Access Key (e.g., WebCrypto P256 key) - User's Access Key signs the transaction
- Transaction includes the
key_authorizationAND the Access Keysignature - Protocol validates Passkey signature on
key_authorization, setstransactionKey[account] = 0, callsAccountKeychain.authorizeKey(), then validates Access Key signature - Transaction executes with Access Key's spending limits enforced via internal
verify_and_update_spending()
Subsequent Access Key Usage
- User's Access Key signs the transaction (no
key_authorizationneeded) - Protocol validates the Access Key via
validate_keychain_authorization(), setstransactionKey[account] = keyId - Transaction executes with spending limit enforcement via internal
verify_and_update_spending()
Root Key Revoking an Access Key
- User signs Passkey prompt → signs transaction calling
revokeKey(keyId) - Transaction executes, marking the Access Key as inactive
- Future transactions signed by that Access Key will be rejected