<!--
Blockchain Lab reading mirror.
Licence: CC0-1.0. This is the upstream licence, not a Blockchain Lab grant.
Canonical: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8130.md
Repository: https://github.com/ethereum/EIPs
Mirrored: 2026-10-10
The text below is the upstream file. The desk study is a different page and is not covered by this licence.
-->

---
eip: 8130
title: Keystore Accounts
description: An onchain keystore that authorizes account actors through authenticator contracts, usable by any transaction transport
author: Chris Hunter (@chunter-cb) <chris.hunter@coinbase.com>
discussions-to: https://ethereum-magicians.org/t/eip-8130-account-abstraction-by-account-configurations/25952
status: Draft
type: Standards Track
category: Core
created: 2025-10-14
requires: 170, 1014
---

## Abstract

This proposal defines the Keystore: an onchain account configuration system in which each account registers **actors**, each bound to an **authenticator** contract that verifies signatures, with a per-actor scope, expiry, and optional policy. Authenticators answer "who signed"; the Keystore answers "what may that actor do". The Keystore is independent of any one transaction format. It is usable through ordinary EVM calls on any chain, through [ERC-4337](./eip-4337.md), by [EIP-8141](./eip-8141.md) frame accounts, and natively by the AA Transaction Type proposal, whose integration this document specifies. This document specifies the Keystore's protocol-visible state, the authorization model, and how consumers integrate with it.

## Motivation

Account abstraction proposals that delegate validation to wallet code force nodes to simulate arbitrary EVM before accepting a transaction. This requires full state access, tracing infrastructure, and reputation systems to bound the cost of invalid submissions.

This proposal separates authentication from account logic. Each authorization declares its authenticator, a contract that takes a hash and signature data and returns the authenticated actor. Validation is predictable: wallets know the rules, and a consumer can see exactly what computation an authorization requires before running it. Instead of simulating arbitrary code, nodes filter on authenticator identity, accepting a small, standard canonical set.

New signature algorithms are introduced through authenticator contracts and standardized through the canonical authenticator set, so accounts are crypto-agile without migration.

Portability is also a top concern. The Keystore lets accounts manage their authenticators with one set of contracts and one signed change format across chains and transports.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

### Overview

An account manages its authentication configuration via the Keystore, a contract at `KEYSTORE_ADDRESS` that maps an account to its configured actors. An **actor** on the account is one that has an authorization in the Keystore. Each actor is bound to an **authenticator**, an onchain contract that checks a signature and returns the actor's identity (`actorId`). The Keystore then authorizes the actor by its scope.

A **consumer** is any system that authorizes actions for an account using the Keystore: a transaction type that validates against it natively, a frame account whose code calls it, an [ERC-4337](./eip-4337.md) account, or an application verifying signatures (see [Native Integration](#native-integration)).

This EIP specifies the Keystore's protocol-visible behavior and state. The Keystore contract and the canonical authenticator and account contracts, including exact storage packing, typehashes, event ABIs, and function bodies, live in the canonical contracts repository (the `base` organization's EIP-8130 repository on GitHub, `src/Keystore.sol`), which is authoritative for all contract internals. Where this document gives a byte layout or digest, it is a normative summary; any implementation MUST match the repository, either by calling the deployed contracts or by reproducing their behavior.

#### Account Types

| Account Type | How It Works | Key Recovery |
|--------------|--------------|--------------|
| **EOAs** | An EOA's own secp256k1 key is an implicit admin actor (the self-actor), with no Keystore write. The account MAY add actors, restrict or revoke the self-actor, or lock | Wallet-defined; EOA recoverable via standard transactions |
| **Existing Smart Contracts** | Already-deployed accounts (e.g., [ERC-4337](./eip-4337.md) wallets) call `importAccount()` themselves; the wallet's code returns the actor set to install via `confirmKeystoreImport()` | Wallet-defined |
| **New Accounts (No EOA)** | Created via `createAccount` (or a consumer's native create path) with CREATE2 address derivation; runtime bytecode placed at the address and initial actors configured | Wallet-defined |

### Authenticators

Each actor is associated with an authenticator, a contract that performs signature authentication. The authenticator *authenticates* the actor (it returns the actor's `actorId`); scope and policy on the Keystore then *authorize* what that actor may do. The authenticator address is stored in `actor_config`. All authenticators implement `IAuthenticator.authenticate(hash, data)`. After the authenticator returns an `actorId`, the consumer validates it against `actor_config` and checks its scope against the authorization context.

Any contract implementing `IAuthenticator` can be permissionlessly deployed and registered as an actor's authenticator on any chain. Registration always makes the authenticator usable within EVM execution (for example, authenticating an actor via `applySignedAccountChanges`, enabling wallet-defined recovery methods). Whether a consumer accepts it on a transaction validation path is that consumer's policy.

#### Canonical Authenticator Set

The canonical authenticator set is the set of signature algorithms every consumer that validates natively MUST accept, at a constant enshrined cost. The initial set:

| Name | Algorithm | Authenticator | `actorId` Derivation |
|------|-----------|----------|----------------------|
| k1 | secp256k1 | `K1_AUTHENTICATOR` (native sentinel) | `bytes32(uint256(uint160(recovered_address)))` (the address right-aligned into a 32-byte word) |
| p256 | P-256 | Onchain contract | `keccak256(abi.encodePacked(x, y))` |
| passkey | WebAuthn / FIDO2 | Onchain contract | `keccak256(abi.encodePacked(x, y))` |
| delegate | Signature delegation | Onchain contract | `bytes32(uint256(uint160(delegated_address)))`: signatures from `delegated_address` are valid for the registering account (see [Delegate Authenticator](#delegate-authenticator)) |

The canonical authenticators are deployed at deterministic CREATE2 addresses across chains and catalogued in a companion ERC (number TBD). When a canonical authenticator is enshrined, its execution MUST produce results identical to the corresponding contract. On a base layer the enshrined set changes only through a hard fork; other chains MAY extend it through the companion ERC process.

The set is expected to grow as signature algorithms mature (e.g., post-quantum). This does not disturb existing accounts: actors reference their authenticator by address, so an account adopts a newly canonical scheme by authorizing a new actor pointing at it, with no migration, redeployment, or address change.

#### Delegate Authenticator

The delegate authenticator lets one account act on behalf of another: account **A** registers a delegate actor with `actorId = bytes32(uint256(uint160(B)))`, after which any key that can authenticate as account **B** may authenticate as **A**, bounded by the scope **A** grants that actor. Delegation MUST NOT chain (the nested authenticator MUST NOT itself be the delegate authenticator) and the nested authenticator MUST be canonical, keeping total validation work bounded. The remaining details are defined in the canonical repository.

### Keystore and Account Configuration

Each account can authorize a set of actors through the Keystore contract at `KEYSTORE_ADDRESS`. This contract handles actor authorization, account creation, change sequencing, and account lock, and delegates signature authentication to [Authenticators](#authenticators). Its full ABI, storage packing, typehashes, and events are defined in the canonical repository; this section specifies the protocol-visible behavior and the state a consumer reads directly.

Actors are identified by their `actorId`, a 32-byte identifier derived by the authenticator from public key material. Actors are modified through signed account-change batches (see [Account Changes](#account-changes)).

Actor enumeration is performed off-chain via `ActorAuthorized` and `ActorRevoked` event logs.

#### Storage Layout

Each actor occupies a single `actor_config` slot containing the authenticator address, an optional expiry, and the scope bitmask. Consumers read this slot directly, so its packing is normative: `authenticator(20) ‖ expiry(6) ‖ scope(2) ‖ reserved(4)`, where `expiry` is a `uint48` Unix timestamp in **seconds** (`0` = no expiry; the actor is invalid once `block.timestamp > expiry`) and `scope` is the `uint16` permission bitmask (`0x0000` = unrestricted, also the admin predicate; see [Actor Scope](#actor-scope)). The exact slot derivation and byte offsets are defined in the canonical repository.

An actor MAY also carry a signed policy commitment and a manager address in the separate policy slots `policy_commitment`/`policy_manager` (see [Actor Policies](#actor-policies)). Whether those slots are written is decided by the **length** of the `policyData` the authorizing change carried (empty, or exactly 52 bytes), not by any scope bit; whether a consumer *gates* the actor on them is decided by the `POLICY` scope bit (`0x08`). Actors are revoked by deleting the `actor_config` slot. The self-actor (`actorId == bytes32(uint256(uint160(account)))`) is the one exception: it is held inline in the packed account-state slot (see [Signature Verification](#signature-verification)).

```
policy_commitment(account, actorId) → bytes32   // set when policyData was attached (52 bytes)
policy_manager(account, actorId)    → address   // set when policyData was attached (52 bytes)
```

These slots are read only during execution (see [Actor Policies](#actor-policies)); validity is decided by the single `actor_config` read, giving cheap, predictable validation and invalidation.

#### Actor Scope

The scope field in `actor_config` is a `uint16` permission bitmask of **grants**. A value of `0x00` means unrestricted (all contexts) and is also the account's **admin** predicate: any check in this specification that requires an "admin" actor is exactly `scope == 0x00`. Any non-zero value grants only the contexts whose bits are set. Reads are fail-closed: a context is authorized only when `scope == 0x00` or the corresponding grant bit is present. Unknown bits grant nothing. All future scope bits MUST be pure grants. An actor whose scope sets only unknown bits is stored verbatim but authorizes no context.

| Bit | Value | Name | Context |
|-----|-------|------|---------|
| 0 | `0x01` | OPERATOR | Ungated initiation: may originate transactions to any target. Carries **operational** authority, which also governs account-level signing ([ERC-1271](./eip-1271.md)); see [Signature Verification](#signature-verification) |
| 1 | `0x02` | SELF_PAYER | Self-pay gas: authorizes paying the account's own gas |
| 2 | `0x04` | SPONSOR_PAYER | Sponsor gas: authorizes paying gas for a different sender |
| 3 | `0x08` | POLICY | Gated initiation: may originate transactions only to the actor's `policy_manager` (see [Actor Policies](#actor-policies)). Optional grant |
| 4 | `0x10` | NONCE | Permits a restricted actor to use the consumer's sequenced nonce channels (see [Actor Nonce Scope](#actor-nonce-scope)). Optional grant |
| 5–15 | | (spare) | Reserved for future pure grants |

The canonical bit assignment is `Scopes.sol` in the canonical repository; this table MUST match it.

**Admin.** Config-change authority (`authorizeActor`, `revokeActor`, `applySignedAccountChanges`, code delegation) is exactly the admin predicate `scope == 0x00`. Accounts are born with a scope-0 root (the implicit EOA, or an unrestricted actor named at create/import); everything below that root is granted.

**Initiation grants.** `OPERATOR` and `POLICY` are the two initiation grants and do **not** combine into a third behavior: `OPERATOR` allows initiation to any target; `POLICY` allows gated initiation to the actor's `policy_manager` only. `OPERATOR` is not suppressed by `POLICY`: an actor holding `OPERATOR | POLICY` is an ungated operator, so a policy-gated key MUST NOT carry `OPERATOR`. Wallets SHOULD NOT set `OPERATOR | POLICY`. `POLICY | SELF_PAYER` and `POLICY | NONCE` compose. `POLICY | SPONSOR_PAYER` also composes: initiation is gated to the manager, but sponsor authority is not gated. `authorizeActor` stores any scope combination verbatim; combination semantics are checked at the point of use. In short: *length decides what gets stored; `POLICY` decides whether the sender is gated; `OPERATOR` overrides `POLICY`.*

#### Actor Nonce Scope

`NONCE` (`0x10`) grants a restricted actor access to a consumer's **sequenced** nonce channels for sender-context authorization; it does not apply to payer authorization. A consumer whose nonce system has no sequenced/unsequenced distinction ignores it.

| Actor | Nonce channels allowed |
|-------|------------------------|
| Admin (`scope == 0x00`) | Any, including unsequenced (nonce-free) |
| Restricted, `NONCE` unset | Unsequenced (nonce-free) only |
| Restricted, `NONCE` set | Any |

#### Actor Expiry

An actor is **live** at `now` (the inclusion block timestamp) iff `expiry == 0 || now <= expiry`, evaluated at inclusion. Only the acting key's own `expiry` is checked; authorizer lineage is never walked, so a live admin's authorization survives that admin's own later expiry or revocation. Removal requires an explicit `RevokeActor`.

Note for wallets: `expiry` is set only by a config change (initial actors are always non-expiring), and SHOULD be placed only on non-admin actors. Using it on an admin can break valid change chains for cross-chain replay and account sync, so an admin SHOULD NOT expire unless the wallet keeps a non-expiring owner in place.

#### Epoch System

To enable uncoordinated session-key, subscription, and just-in-time (JIT) actor additions, the local change channel carries an **epoch** (`local_epoch`) alongside its sequence counter (`local_sequence`); together they form the signed local word `local_epoch(high 32) || local_sequence(low 32)` (see [Change Authorization](#change-authorization)). The epoch is a blunt cancellation control for the account's own outstanding **local** signatures:

- `IncrementLocalEpoch` increments `local_epoch` and resets `local_sequence` to `0`. Every local batch signed at the prior epoch, sequenced or unsequenced, is rejected with `StaleEpoch`.
- The epoch does **not** revoke live actors or touch `actor_config`. Only *unlanded local signatures* are invalidated.
- Multichain batches are unaffected: they carry no epoch.

This is what makes the unsequenced/JIT mode (`UNSEQUENCED`) safe to hand out: an unsequenced batch is replayable within its epoch, and the account durably retires it by bumping the epoch, typically batching the reducing `RevokeActor`/`AuthorizeActor` with `IncrementLocalEpoch`.

#### Actor Policies

Actor policies gate a key to a single `manager` contract that enforces application-defined authority on the account's behalf, for example a session key limited to a spend cap or a small set of targets.

Two independent decisions are involved, and the Keystore contract makes only the first:

- **What is stored** is decided by the **length** of `policyData` on the authorizing change. `policyData` MUST be either empty (config only) or exactly `manager` (20 bytes) ‖ `commitment` (32 bytes), written verbatim to `policy_manager` / `policy_commitment`; any other length is rejected. A zero `commitment` is a valid "no parameters" value, and a zero `manager` gates the key to `address(0)`, leaving no productive target. An authorize with empty `policyData` clears any prior policy slots.
- **Whether the sender is gated** is decided by the **consumer** from the `POLICY` scope bit and the absence of `OPERATOR`. A `POLICY` actor with no stored `manager` is gated to `address(0)`; an actor with stored policy but no `POLICY` bit is not gated.

| `POLICY` set (no `OPERATOR`) | Gate: every target MUST equal | Top-level value | `policyData` at authorize |
|----------------|----------------------------|-----------------|---------------------------|
| no | (no gate) | allowed | empty, or `manager` (20) ‖ `commitment` (32) |
| yes | the stored `manager` (`address(0)` = no productive target) | MUST be `0` | empty, or `manager` (20) ‖ `commitment` (32) |

A policy-bearing actor may call exactly one target: its configured `manager`, which reads the actor's `commitment` (via `getPolicyCommitment` or `getActorWithPolicy`), validates presented parameters against it, enforces what the call may do, and carries out the approved action. A policy-gated actor cannot move ETH as top-level transaction value; ETH movement goes through the manager and account code. The consumer's only responsibilities are the single-target gate and the value restriction.

A key that should be enforced by the account's own code rather than a separate contract sets `manager = account`. Because the call originates from the account itself, this is only meaningful with policy-aware wallet code; code that implicitly trusts self-calls (e.g. a standard `executeBatch`) turns such a key into an unrestricted one.

**Example** (non-normative). A subscription session key limited to 5 USDC per 30-day period with a two-target allowlist, authorized with `POLICY | SELF_PAYER`, the manager, and a commitment to `(USDC, 5_000_000, period)`. Over-budget transfers, wrong targets, expiry, or revocation all fail.

**Lifecycle.** Policy state is keyed by `(account, actorId)`; `revokeActor` clears it.

#### Account Lock

Account lock state is stored in a single packed 32-byte account-state slot that also holds the change channels, an account-flags byte, and the inline self-actor config. Consumers may read this slot directly (for example for mempool tiering), so its field order and widths are normative.

When `LOCKED`, all authority changes (authorize/revoke actor) are rejected on every path, and consumers MUST reject code delegation for the account. Only two changes remain permitted while locked: `Unlock` (which begins the timed release) and `IncrementLocalEpoch`. The stored `unlock_delay` is bounded to the `uint16` range (~18.2h max): lock exists for mempool permissioning, and a short ceiling prevents an account from bricking config rotation.

Lock and unlock are **local-only** changes carried in a signed batch through [`applySignedAccountChanges`](#account-changes); there is no separate lock function. Each is admin-authorized, binds `chainId = block.chainid`, and is consumed against the local channel. `Lock` and `Unlock` MUST each be the **sole** change in their batch.

1. **Lock**: only from the unlocked state. Sets `LOCKED` and stores `unlock_delay` (a zero delay is rejected). Emits `AccountLocked`.
2. **Unlock**: only from `LOCKED` with no pending unlock. Sets `UNLOCK_INITIATED` and `unlocks_at = block.timestamp + unlock_delay`. Emits `AccountUnlockInitiated`.
3. **Effective unlock**: once `block.timestamp >= unlocks_at`, the account is unlocked; flags are lazily cleared by the next operation. Locking again requires a fresh `Lock`.

### Account Creation

New accounts are created with pre-configured actors by `createAccount(userSalt, bytecode, initialActors)` or by a consumer's native create path, which MUST produce identical results. The `code` is placed directly at the account address and is not executed during deployment; initialization logic runs afterwards.

Each initial actor carries an `actorId`, `authenticator`, `scope` (`0x0000` = admin), and `policyData` (empty, or exactly `manager ‖ commitment`), all committed to the derived address so the counterfactual address binds each initial actor's authority. Two things are **not** expressible at create: `expiry`, and a self-referential `manager = account`. Both are added afterwards with a config change, which MAY accompany creation in the same transaction.

#### Address Derivation

Addresses use the [EIP-1014](./eip-1014.md) CREATE2 formula with `KEYSTORE_ADDRESS` as the deployer. `initial_actors` MUST be sorted by `actorId` in strictly ascending order, which makes derivation deterministic and rejects duplicates:

```
leaf_i            = keccak256(actorId_i || authenticator_i || scope_i || policyData_i)
actors_commitment = keccak256(leaf_0 || leaf_1 || ... || leaf_n)

effective_salt  = keccak256(user_salt || actors_commitment)
deployment_code = DEPLOYMENT_HEADER(len(code)) || code
address = keccak256(0xff || KEYSTORE_ADDRESS || effective_salt || keccak256(deployment_code))[12:]
```

`computeAddress` MUST apply the same initial-actor validation as create (non-empty set, strictly ascending non-zero `actorId`s, each `authenticator >= K1_AUTHENTICATOR`, `policyData` empty or exactly 52 bytes), so a predicted address always corresponds to an actor set that create will accept.

`DEPLOYMENT_HEADER(n)` is a fixed 14-byte EVM loader that returns the trailing code (see [Appendix: Deployment Header](#appendix-deployment-header)). Through `createAccount`, `deployment_code` is passed as init code to CREATE2; a native create path constructs the same `deployment_code` for derivation but places `code` directly.

#### Creation Rules

1. Validate `initial_actors`: each `policyData` empty or exactly 52 bytes; `scope` stored verbatim; strictly ascending `actorId`.
2. Derive the address per [Address Derivation](#address-derivation).
3. Require CREATE2 freshness at the address: `code_size == 0` and `nonce == 0`.
4. Place `code`, register `initial_actors`, set `local_sequence = 1` (initialized), initialize lock state to unlocked, and set `DEFAULT_EOA_REVOKED` unless the self-actor is among `initial_actors`. Emit `AccountCreated`.

### Account Changes

All actor and lock changes are carried in a signed **account-change batch** (`SignedAccountChanges`), applied through the Keystore's single entry point `applySignedAccountChanges(account, batch)` or through a consumer's native path carrying the same batch. The batch binds a replay `channel` and a `sequence`, and applies its ordered list of changes atomically.

```
SignedAccountChanges = (
  channel,            // 0 = Local (binds block.chainid), 1 = Multichain (binds chain_id 0)
  sequence,           // Local  = local_epoch(high 32) || local_sequence(low 32)
                      // Multichain = a plain monotonic counter
  changes,            // ordered list of (change_type, payload)
  signature           // admin (scope == 0x00): authenticator || data
)
```

The `payload` is ABI-encoded so the same bytes decode identically whether applied natively or through the contract.

| change_type | Name | `payload` | Description |
|-------------|------|-----------|-------------|
| `0` | `AuthorizeActor` | `abi.encode(bytes32 actorId, ActorConfig config, bytes policyData)` | Upsert an actor: writes `actor_config` (authenticator, expiry, scope verbatim, reserved bytes zeroed). `policyData` MUST be empty (clears the policy slots) or exactly 52 bytes (writes them). Emits `ActorAuthorized`, whose `actorData` is 32 bytes without policy or 84 bytes with it |
| `1` | `RevokeActor` | `abi.encode(bytes32 actorId)` | Revoke an actor. For a non-self actor, deletes `actor_config` and the policy slots. For the self-actorId, a k1 self is revoked by setting `DEFAULT_EOA_REVOKED`, and a non-k1 self has its `actor_config` deleted. Emits `ActorRevoked` |
| `2` | `IncrementLocalEpoch` | empty | **Either channel.** Increments `local_epoch` and resets `local_sequence`. Consumes no sequence itself. See [Epoch System](#epoch-system) |
| `3` | `Lock` | `abi.encode(uint16 unlockDelay)` | **Local only, standalone.** See [Account Lock](#account-lock) |
| `4` | `Unlock` | empty | **Local only, standalone.** See [Account Lock](#account-lock) |

#### Change Authorization

Each batch is authorized by a single **admin** signature over the batch digest, in `authenticator || data` form. When batches are applied in sequence, each must be valid against the actor state after all previous batches have been applied. Anyone may relay; authorization comes from the signature.

- **Multichain** (`chain_id 0`): a plain monotonic `uint64` counter, valid on any chain, for synchronizing actor state across chains. `sequence` MUST equal the current value, which then increments. There is no epoch or unsequenced mode on this channel, though a Multichain batch MAY carry `IncrementLocalEpoch`.
- **Local** (`block.chainid`): `sequence` is `local_epoch || local_sequence`. The batch is rejected with `StaleEpoch` unless the high half equals the current `local_epoch`.
  - **Sequenced** (low half `< UNSEQUENCED`): the low half MUST equal the current `local_sequence`, which then increments.
  - **Unsequenced / JIT** (low half `== UNSEQUENCED`, i.e. `uint32` max): consumes no counter and remains replayable until the epoch moves. Ordering between unsequenced batches is undefined.

**Expiry on `AuthorizeActor` never reverts.** On the unsequenced path, an already-lapsed grant is **silently skipped**, so replaying an old expired JIT grant can never clobber a renewed one. A single-consume batch (Multichain, or Sequenced local) installs an already-lapsed grant **inert** (written but not live) and still consumes its sequence, so a chain catching up on multichain history never strands its counter. A zero `expiry` is always accepted.

The combined local word doubles as the initialized flag: creation and import set `local_sequence = 1`, so an all-zero word means uninitialized.

The batch digest is a typed (ABI-encoded, [EIP-712](./eip-712.md)-style) struct hash binding `account`, the resolved `chainId` (`0` for Multichain, else `block.chainid`), the `sequence` word, and the ordered `changes`. It cannot collide with transaction signature hashes of the form `keccak256(type_byte || rlp([...]))`. The exact typehashes are defined in the canonical repository.

`applySignedAccountChanges` parses the authenticator from `signature`, resolves the admin `actorId`, verifies the digest, and applies each change in order. Authority changes are rejected while the account is locked.

### Account Import

`importAccount()` is a one-time call that registers an already-deployed account into the Keystore with an initial actor set. It is not signature-relayable and takes no parameters: the account itself MUST be `msg.sender`, and the actor set comes from the account's own code:

```solidity
interface IKeystoreImport {
    /// Returns this account's intended import actor set and `computeImportDigest(account, initialActors)`.
    function confirmKeystoreImport() external view returns (bytes32 digest, InitialActor[] memory initialActors);
}
```

The Keystore `STATICCALL`s `confirmKeystoreImport()` on `msg.sender`, validates the returned set (non-empty, strictly ascending `actorId`s, `authenticator >= K1_AUTHENTICATOR`, `policyData` empty or 52 bytes), and installs it only when `digest == computeImportDigest(account, initialActors)`. The call is rejected when:

- The account already has Keystore state: any change channel or epoch is non-zero. A locked account is always caught here, since `Lock` advances `local_sequence`.
- `confirmKeystoreImport()` reverts, cannot be decoded, or returns a mismatched digest (`ImportNotConfirmed`). A malformed set fails with its specific error (`NoInitialActors` / `ActorsNotSortedOrDuplicate` / `InvalidAuthenticator` / `InvalidPolicyData`) first; this ordering is normative.

Import carries no `chainId`; a wallet that wants to bind import to a chain checks `block.chainid` inside `confirmKeystoreImport`. The digest binds the account's own actorId and each initial actor with `expiry = 0`. Unlike create, `manager = account` is expressible. On success `importAccount` sets `DEFAULT_EOA_REVOKED`; to keep using the native key, include the self-actorId as a `K1_AUTHENTICATOR` entry.

Because `importAccount` is reachable through any `execute()`-style path the wallet exposes, a wallet SHOULD gate the return of a valid `(digest, actors)` pair on its own owner-controlled import entry, or accept that anyone who can drive `execute` can finalize the import. After import, the Keystore is the source of truth for the actor set.

### Signature Verification

The Keystore exposes one raw primitive, `authenticateActor(account, hash, auth)`, which maps a hash and signature to a verified `(actorId, scope)`. Wallet-originated flows (transactions, account changes, other protocol paths) authenticate directly against it over their own digest. With `auth = authenticator ‖ data` it:

1. **Authenticates.** For `K1_AUTHENTICATOR` (`address(1)`), ecrecover natively, giving `actorId = bytes32(uint256(uint160(recovered)))`. For any other authenticator, call `authenticate(hash, data)` via `STATICCALL`, returning `actorId` (`bytes32(0)` is invalid). `address(0)` is never a valid authenticator.
2. **Resolves.** If authentication used the native secp256k1 path and `actorId` is the account's own address, resolve the self-actor from the inline config in the packed account-state slot: reject if `DEFAULT_EOA_REVOKED` is set; otherwise take `scope` and `expiry` from the inline fields (all-zero = unrestricted, non-expiring admin). Otherwise read `actor_config(account, actorId)`, reject if its reserved bytes are non-zero, and require that the stored authenticator equals the one used. Generic authenticator contracts never satisfy the self-actor branch, even if they return the account's own actorId.
3. **Checks liveness.** Reject if `expiry` is non-zero and `block.timestamp > expiry`.

Applications that request a signature (Sign-In with Ethereum, `Permit`, order flow) use `validateSignature(account, hash, auth)`. This wraps `authenticateActor` with an identity binding: `replaySafeHash(account, chainId, hash) = keccak256(SIGNED_MESSAGE_TYPEHASH, account, chainId, hash)` binds the signature to an account and chain (`chainId = 0` binds all chains). The `auth` blob is `sigType(1) ‖ authenticator(20) ‖ data`, where `sigType` selects the **Local** (`0x01`) or **Multichain** (`0x02`) domain.

Both methods return `(actorId, scope)` rather than a boolean, so a consumer can attribute a signature to a specific key and make a granular authorization decision.

This supersedes [ERC-1271](./eip-1271.md) while remaining compatible: an account implements `isValidSignature(hash, signature)` on top of `validateSignature`, returning the magic value when the resolved actor is operational (admin, or any scope carrying `OPERATOR`) and treating any revert as invalid.

**Native signature verification.** ERC-1271 verifies by calling back into the account, which a precompile cannot do. The canonical authenticator set is small and enshrinable and authority lives in a flat `actor_config` slot, so `validateSignature` can run entirely in native code and be exposed as a precompile, producing identical results to EVM execution.

### Native Integration

A consumer MAY integrate the Keystore natively instead of calling the contract, either fully enshrined (native Keystore reads, account-change application, and canonical authenticators) or only as a mempool optimization over EVM execution. Either way, authentication MUST be equivalent to `authenticateActor(account, hash, auth)`, and the resolved actor's scope, expiry, policy, and the account's lock apply as specified in [Actor Scope](#actor-scope), [Actor Expiry](#actor-expiry), [Actor Policies](#actor-policies), and [Account Lock](#account-lock). All Keystore and canonical authenticator contracts are deployed at deterministic CREATE2 addresses across chains. [AA Transaction Type Integration](#aa-transaction-type-integration) specifies one native consumer in full.

### AA Transaction Type Integration

The AA Transaction Type proposal is designed to integrate with the Keystore directly. This section applies on chains where both are active. It fills that transaction type's extension points (the authenticator selector in `sender_auth` / `payer_auth` and the resolved-actor hook) and assigns account-change types `0x02` and `0x03` from its reserved range. It does not change its wire format or signature payloads, or the behavior of any account whose Keystore state is empty. Field names, `K1_AUTH_COST`, `MAX_AUTHENTICATION_GAS`, `intrinsic_gas`, and `payer_gas_reserve` are as defined there.

#### Actor Resolution

A named sender or payer (`sender_auth` / `payer_auth` is `authenticator || data`) resolves exactly as `authenticateActor` does (see [Signature Verification](#signature-verification)), over the transaction's sender or payer signature hash, with a non-native authenticator's `STATICCALL` bounded by `MAX_AUTHENTICATION_GAS`. A recovered sender or payer (empty `sender`, open payer) resolves to the recovered address's inline self-actor.

The transaction type's rule for named identities without the Keystore ("recovered address MUST equal the account") is exactly the self-actor case of this resolution.

#### Authenticators on the Transaction Path

Every chain accepts the canonical authenticator set on this transaction path at a constant enshrined cost. Whether other authenticators whose role permits transaction authentication are accepted is the chain's **acceptance policy**:

- **Permissive** (typically L1): any such authenticator whose `STATICCALL` returns within `MAX_AUTHENTICATION_GAS`, metered as ordinary EVM execution.
- **Canonical-only** (typically L2): only the canonical set. Other authenticators remain usable through EVM paths.

A permissive policy admits authenticators that may read mutable state, so nodes MUST track the state they touch and revalidate on change.

#### Authorization

The resolved actor's scope is checked per [Actor Scope](#actor-scope):

- **Sender**: initiation authority (admin, `OPERATOR`, or `POLICY`; `POLICY` without `OPERATOR` gates the actor to its manager).
- **Nonce**: when `nonce_key != NONCE_KEY_MAX`, the sender actor MUST be admin or hold `NONCE`.
- **Payer**: when the resolved payer account equals the sender (implicit self-pay, explicit self-pay, or an open payer that recovers to the sender), the actor authorizing payment MUST hold `SELF_PAYER`. Otherwise the payer actor MUST hold `SPONSOR_PAYER`. Implicit self-pay uses the sender actor; the other forms use the `payer_auth`-resolved actor.
- **Delegation entry**: in addition to the transaction type's own delegation-entry rules, the sender actor MUST be admin and the account MUST NOT be locked.

#### Create Entry (`0x02`)

```
rlp([
  0x02,               // type: create
  user_salt,          // bytes32
  code,               // bytes: runtime bytecode placed at the account address
  initial_actors      // [[actorId, authenticator, scope, policyData], ...], strictly ascending actorId
])
```

- At most one create entry, and it MUST be the first entry.
- `sender` MUST equal the address derived per [Address Derivation](#address-derivation), and the address MUST satisfy CREATE2 freshness (`code_size == 0` and account nonce `0`) in the pre-transaction state, before a `nonce_key` `0` transaction increments the account nonce.
- `sender_auth` MUST authenticate as one of `initial_actors` (matching `actorId` and authenticator).
- Application places `code` at `sender` without executing it, registers `initial_actors` in the Keystore, and initializes the account per [Account Creation](#account-creation). Initialization logic runs in `calls`.

#### Config Change Entry (`0x03`)

```
rlp([
  0x03,               // type: config change
  channel,            // uint8: 0 = Local, 1 = Multichain
  sequence,           // uint64
  changes,            // [[change_type, payload], ...]
  auth                // admin signature over the batch digest: authenticator || data
])
```

The entry carries one signed account-change batch (see [Account Changes](#account-changes)), with the same channels, sequencing, change types, digest, and authorization as `applySignedAccountChanges`. Each batch's `auth` is validated against the actor state after all previous entries are applied. Already-applied entries are skipped. Nodes SHOULD enforce a configurable per-transaction limit on config change entries.

#### Execution Order

Entries are applied in list order before `calls`, and `sender_auth` is validated against the actor state that results from applying them. A transaction with a config change or delegation entry for a locked account is invalid, except for changes permitted while locked (see [Account Lock](#account-lock)).

#### Policy Gate

When the sender actor is policy-gated, the protocol resolves its manager once at the start of `calls` execution; that snapshot gates every call in every phase. A call whose `to` is not the manager is not dispatched and fails with:

```solidity
error ActorPolicyViolation(bytes32 actorId, address target);
```

This is an execution result, not a validity error: the enclosing phase reverts, later phases are skipped, and only work already performed is charged.

#### Value Rules

These are validity rules, checked from signed fields and state already read during validation:

1. If the sender actor is policy-gated (`POLICY` set, `OPERATOR` unset), every `call.value` MUST be `0`.
2. If `sender`'s code is canonical account bytecode (see [High-Rate Payers](#high-rate-payers)), every `call.value` MUST be `0`.

Neither rule can apply to an account with empty Keystore state authenticated through its own secp256k1 key: such an actor is not policy-gated, and an account with placed canonical bytecode has no secp256k1 key.

#### Intrinsic Gas

| Component | Value |
|-----------|-------|
| `sender_auth_cost` / `payer_auth_cost` | Canonical authenticator: its enshrined constant cost. Other accepted authenticator: metered `STATICCALL` execution plus one `actor_config` read (2,100). The secp256k1 self-actor cost is unchanged at `K1_AUTH_COST`. For `payer_auth` this replaces `K1_AUTH_COST`; `payer_data_cost` and the calldata floor still apply |
| `bytecode_cost` | For a create entry: 32,000 plus 200 per deployed byte |
| `account_changes_cost` | Per create entry: 22,100 per initial actor slot write, plus policy slot writes for actors with `policyData`. Per applied config change entry: `auth` authentication cost plus the storage writes it performs. Per skipped config change entry: 2,100 |

`bytecode_cost` is added to `intrinsic_gas`. When payer authentication is metered execution rather than an enshrined constant, `payer_auth_cost` is unknown until it runs, so `payer_gas_reserve` is `MAX_AUTHENTICATION_GAS`, and the authenticator's `STATICCALL` is capped at `MAX_AUTHENTICATION_GAS - 2,100 - 10 * payer_tokens` so the payer's floored share stays within it. A chain MAY implement Keystore reads, account-change application, and canonical authenticators natively at these fixed costs; results MUST match executing the Keystore contract.

#### Transaction Context Precompile

This integration adds the following functions to the AA Transaction Type's Transaction Context precompile, alongside `getTransactionPayer()`. Gas is the same: a base cost plus 3 gas per 32 bytes of returned data. Outside an `AA_TX_TYPE` transaction, each of these returns the zero value.

| Function | Returns |
|----------|---------|
| `getTransactionSender()` | `address`, the resolved sender |
| `getTransactionCalls()` | `Call[][]`, the full `calls` array |
| `getTransactionGasLimit()` | `uint256`, `gas_limit` |

The precompile, including `getTransactionPayer()`, is also readable during authentication, so a payer authenticator can read `calls` and `gas_limit` to decide onchain whether to sponsor. A chain MAY populate it during execution only.

#### Receipt Logs

The protocol SHOULD inject log entries matching the Keystore events (`ActorAuthorized`, `ActorRevoked`, `AccountCreated`, and others) for account changes applied by an AA transaction.

#### Mempool

For canonical authenticators, a validated transaction is invalidated only by `actor_config` changes, nonce consumption, and payer balance. Nodes MAY apply higher pending limits to accounts that qualify for the [high-rate payer](#high-rate-payers) tier.

#### RPC: `eth_estimateGas` and `eth_call`

The AA Transaction Type's requests default to the account's own secp256k1 key. With this integration, a request selects a Keystore actor for the sender or payer with either or both of:

- `senderActorId` / `payerActorId` (`bytes32`): the actor to simulate.
- `senderAuth` / `payerAuth` in the named form `authenticator || data`, where `data` is a dummy of the length a real signature would have.

The node resolves the simulated actor without verifying any signature:

1. **Actor given.** Read `actor_config(account, actorId)`, or the inline self-actor when `actorId` is the account's own address. The request fails if the actor does not exist, is revoked, or has expired. If an auth blob is also supplied, its authenticator MUST equal the actor's.
2. **Only an auth blob given.** Use its authenticator. The actor is unknown, so the node applies the most permissive scope the authorization context allows. Scope-dependent checks (policy gate, value rules) are not evaluated. Wallets SHOULD pass the actor ID when the actor is policy-gated.
3. **Neither given.** Use the secp256k1 default of the AA Transaction Type.

When `accountChanges` contains a create entry, the actor MUST be one of its `initial_actors`. Config-change entries are applied before resolution, so an actor they install can be simulated in the same request.

**Authentication gas.** A canonical authenticator is priced at its enshrined constant. For any other authenticator, the node executes it when a real signature is supplied, and otherwise prices authentication at `MAX_AUTHENTICATION_GAS`. When no auth blob is supplied, the node prices a dummy of the resolved authenticator's expected signature length (65 bytes for secp256k1). Auth bytes are priced as non-zero bytes.

**Execution.** Calls run as if the resolved actor had signed. The scope checks, policy gate, and value rules of this section apply, so a request from a policy-gated actor fails the same way the transaction would.

#### Empty Keystore Equivalence

For every account whose Keystore state is empty, validity, execution results, and gas are identical with and without this integration: the inline self-actor is an unrevoked, non-expiring admin, so every authorization rule passes; the value rules cannot apply; and `K1_AUTH_COST` is unchanged.

### High-Rate Payers

A node can grant a higher pending-transaction limit to a payer whose balance can only decrease through gas. An account qualifies when:

- it is `LOCKED`, has no pending unlock (`UNLOCK_INITIATED` clear), and its `unlock_delay` is at or above the node's threshold, so its actor set is frozen for at least that window; and
- its code is **canonical account bytecode**: placed runtime code whose code hash is in the canonical account set, catalogued alongside the canonical authenticator set. Such an account has no secp256k1 key to sign legacy transactions, and its implementation blocks ETH movement through its code while locked. An [EIP-7702](./eip-7702.md) delegation indicator pointing at a canonical implementation does not qualify.

For the tier to hold, a consumer that carries top-level ETH value MUST NOT transfer it from an account with canonical account bytecode; such accounts move ETH through their own code. A node reads the packed account-state slot and the account's code hash to decide eligibility.

### Constants

| Name | Value | Comment |
|------|-------|---------|
| `KEYSTORE_ADDRESS` | CREATE2-derived (resolved at deployment) | Keystore contract address |
| `K1_AUTHENTICATOR` | `address(1)` | Native secp256k1 authenticator (implicit self-actor and explicitly registered k1 actors) |
| `DEFAULT_EOA_REVOKED` | `0x01` | Account-state flag that disables the implicit self-actor path |
| `LOCKED` | `0x02` | Account-state flag that freezes actor configuration |
| `UNLOCK_INITIATED` | `0x04` | Account-state flag selecting the `unlock_delay` vs `unlocks_at` interpretation |
| `UNSEQUENCED` | `2^32 - 1` | Local-channel sentinel for unsequenced (JIT) batches |

### Appendix: Deployment Header

`DEPLOYMENT_HEADER(n)` is a 14-byte EVM loader that copies trailing code into memory and returns it. The header encodes code length `n` into its `PUSH2` instructions:

```
DEPLOYMENT_HEADER(n) = [
  0x61, (n >> 8) & 0xFF, n & 0xFF,     // PUSH2 n        (code length)
  0x60, 0x0E,                          // PUSH1 14       (offset: code starts after 14-byte header)
  0x60, 0x00,                          // PUSH1 0        (memory destination)
  0x39,                                // CODECOPY       (copy code from code[14..] to memory[0..])
  0x61, (n >> 8) & 0xFF, n & 0xFF,     // PUSH2 n        (code length)
  0x60, 0x00,                          // PUSH1 0        (memory offset)
  0xF3                                 // RETURN         (return code from memory)
]
```

Account creation only supports runtime bytecode.

## Rationale

### Why Authenticator Contracts?

Enables signature-algorithm extension through authenticator contracts. The authenticator returns the `actorId` rather than accepting it as input, so the Keystore never needs algorithm-specific logic. All authenticators share a single `authenticate(hash, data)` interface. Actor scope and policy provide role separation without authenticator cooperation.

### Why a Canonical Authenticator Set?

Without a required set, consumers could diverge on which signature algorithms they accept beyond secp256k1, and wallets could not guarantee delivery for other signature types. The canonical set is a shared baseline, expected to remain small, with new algorithms added through the companion ERC process as they gain adoption.

### Why a Transport-Neutral Keystore?

Authority over an account should not depend on which transaction format carries an action. Stating the Keystore once, with every consumer's authentication equivalent to `authenticateActor`, gives an actor the same meaning on a native transaction type, a frame account, an ERC-4337 account, and in signature verification, and lets each transport integrate on its own schedule.

### Why Actor Policies?

Session keys often need narrow authority: only this token, only this much per day. `POLICY` makes gated initiation a first-class grant, distinct from ungated `OPERATOR`: the key may originate transactions, but only to a single target, bound to a signed opaque commitment. Barring top-level value keeps every ETH movement by such a key inside the manager's enforcement. Allowing `POLICY | SELF_PAYER` lets a session key self-pay, which exposes the account's ETH balance to gas spend.

### Why a Local Epoch?

Session keys and JIT authorizations should be usable in any order, and the account should be able to cancel any that have not landed. A single counter forces every signature to burn the next slot. Splitting the local word into `local_epoch || local_sequence` keeps ordered, replay-once changes on the sequence while an epoch bump cancels every unlanded local signature at once. It cancels *signatures*, not *authority*, so it cannot brick a live actor set.

### Why No Public Key Storage?

Authenticators receive the public key in the signature data and derive the `actorId` from it. Per-actor state stays at one `actor_config` read regardless of key size, which matters for large post-quantum keys, and calldata is cheaper than cold storage for material read once per authorization.

### Why Not Reject Scope Combinations at Write Time?

The Keystore is a single immutable CREATE2 deployment and cannot know which grants future versions will define. `AuthorizeActor` stores scope verbatim and validates only timeless structure; combination semantics are checked at the point of use, so an unsatisfiable combination is simply inert.

### Why Account Lock?

Locked accounts have a frozen actor set, so the primary state that can invalidate a validated authorization is nonce consumption. Nodes can cache actor state and apply higher limits. Combined with canonical account bytecode, a locked payer's balance only decreases through gas, supporting high-throughput sponsorship.

### Why Canonical Bytecode Cannot Move Top-Level Value?

Transports that carry native value (such as the AA Transaction Type) would otherwise let a canonical account's own keys move ETH outside its code, breaking the property that ETH leaves a locked canonical account only as gas. Restricting top-level value to non-canonical code keeps that property transport-independent. Only placed code qualifies, because a delegated EOA's key can always move ETH with a legacy transaction.

### Why CREATE2 for Account Creation?

1. **Deterministic addresses**: the same `user_salt + code + initial_actors` produces the same address on any chain.
2. **Pre-deployment funding**: users can receive funds at counterfactual addresses.
3. **Portability**: the same `deployment_code` produces the same address through `createAccount` and native create paths.
4. **Front-running prevention**: `initial_actors` in the salt prevents deployment with different actors.

### External Account Factories

Account creation, import, signed actor changes, and locking are ordinary EVM entry points, so external factories can compose them to mint accounts into a desired end state.

## Backwards Compatibility

No breaking changes. Existing EOAs and smart contracts function unchanged; an EOA's key is an implicit admin actor with no Keystore write. Adoption is opt-in through actor changes, `createAccount`, or `importAccount`.

The `actor_config` layout, the scope-bit assignment with reserved bytes as a version gate, and the address-derivation commitment are defined by this specification; the full contract ABI surface is defined in the canonical repository. There is no prior deployment, so there is no earlier format to migrate.

## Reference Implementation

### IKeystore

The Keystore contract is the canonical ABI surface. Its full source lives in the canonical contracts repository (`src/Keystore.sol`) and is authoritative. The sketch below mirrors the protocol-relevant shape.

```solidity
interface IKeystore {
    // Packed account state (see Account Lock). The signed local word is localEpoch(high 32) || localSequence(low 32).
    struct ChangeSequences {
        uint64 multichain;    // chain_id 0 channel; a plain monotonic counter
        uint32 localEpoch;    // local channel epoch; IncrementLocalEpoch bumps it and resets localSequence to 0
        uint32 localSequence; // local channel counter; low half of the signed local word
    }

    struct ActorConfig {
        address authenticator;
        uint48 expiry;        // Unix seconds; 0 = no expiry. Actor invalid once block.timestamp > expiry
        uint16 scope;         // grants bitmask; 0x0000 = unrestricted (admin). See Actor Scope
    }

    // Actor used for account creation and import. expiry is NOT expressible here.
    // policyData: empty, or manager[20] || commitment[32] (by length, independent of scope).
    struct InitialActor {
        bytes32 actorId;
        address authenticator;
        uint16 scope;
        bytes policyData;
    }

    enum AccountChangeChannel { Local, Multichain }
    enum ChangeType { AuthorizeActor, RevokeActor, IncrementLocalEpoch, Lock, Unlock }
    // Leading byte of a signature envelope: Local (0x01) binds block.chainid, Multichain (0x02) binds chainId 0.
    enum SignatureType { Invalid, Local, Multichain }

    struct AccountChange {
        ChangeType changeType;
        bytes payload;        // AuthorizeActor: abi.encode(bytes32 actorId, ActorConfig, bytes policyData);
                              // RevokeActor: abi.encode(bytes32 actorId); Lock: abi.encode(uint16 unlockDelay); others empty
    }

    struct SignedAccountChanges {
        AccountChangeChannel channel;
        uint64 sequence;      // Local: localEpoch || localSequence; Multichain: monotonic counter
        AccountChange[] changes;
        bytes signature;      // admin (scope == 0x00): authenticator || data
    }

    uint32 constant UNSEQUENCED = type(uint32).max; // JIT sentinel for the local low half

    event ActorAuthorized(address indexed account, bytes32 indexed actorId, bytes actorData);
    event ActorRevoked(address indexed account, bytes32 indexed actorId);
    event AccountCreated(address indexed account, bytes32 userSalt, bytes32 codeHash);
    event AccountImported(address indexed account);
    event LocalEpochIncremented(address indexed account, uint32 localEpoch);
    event AccountLocked(address indexed account, uint16 unlockDelay);
    event AccountUnlockInitiated(address indexed account, uint48 unlocksAt);

    // Account creation (factory) and counterfactual address preview.
    function createAccount(bytes32 userSalt, bytes calldata bytecode, InitialActor[] calldata initialActors) external returns (address);
    function computeAddress(bytes32 userSalt, bytes calldata bytecode, InitialActor[] calldata initialActors) external view returns (address);

    // Import msg.sender. The account's code returns (digest, actors) via IKeystoreImport.confirmKeystoreImport().
    function importAccount() external;
    function computeImportDigest(address account, InitialActor[] memory initialActors) external pure returns (bytes32);

    // Single signed entry point for all account changes (authorize/revoke actor, increment epoch, lock/unlock).
    function applySignedAccountChanges(address account, SignedAccountChanges calldata changes) external;

    // Typed-envelope message signing. auth = sigType(1) || authenticator(20) || data.
    function validateSignature(address account, bytes32 hash, bytes calldata auth) external view returns (bytes32 actorId, uint16 scope);
    function replaySafeHash(address account, uint256 chainId, bytes32 hash) external pure returns (bytes32);
    function envelopeDigest(SignatureType sigType, address account, bytes32 hash) external view returns (bytes32);
    // Lower-level: authenticate an actor over a raw digest (no envelope).
    function authenticateActor(address account, bytes32 hash, bytes calldata auth) external view returns (bytes32 actorId, uint16 scope);

    // Storage views (see the canonical repository for the complete set).
    function getActorConfig(address account, bytes32 actorId) external view returns (ActorConfig memory);
    function getActorWithPolicy(address account, bytes32 actorId) external view returns (ActorConfig memory config, address policyManager, bytes32 policyCommitment);
    function getPolicyManager(address account, bytes32 actorId) external view returns (address);
    function getPolicyCommitment(address account, bytes32 actorId) external view returns (bytes32);
    function getChangeSequences(address account) external view returns (ChangeSequences memory);
    function getLockStatus(address account) external view returns (bool locked, bool hasInitiatedUnlock, uint48 unlocksAt, uint16 unlockDelay);
}
```

### IAuthenticator

```solidity
interface IAuthenticator {
    function authenticate(
        bytes32 hash,
        bytes calldata data
    ) external view returns (bytes32 actorId);
}
```

### ITransactionContext (Precompile)

The full interface under this integration, including the AA Transaction Type's `getTransactionPayer()`:

```solidity
interface ITransactionContext {
    struct Call {
        address to;
        uint256 value;
        bytes data;
    }

    function getTransactionSender() external view returns (address);
    function getTransactionPayer() external view returns (address);
    function getTransactionCalls() external view returns (Call[][] memory);
    function getTransactionGasLimit() external view returns (uint256);
}
```

## Security Considerations

**Validation Surface.** For canonical authenticators, the invalidators of an authorization are `actor_config` changes, lock changes, and the consumer's nonce consumption. Canonical authenticators are pure functions of `(hash, data)` and the resolved `actor_config`, so the invalidator set is small, enumerable, and keyed by the account. Accepting non-canonical authenticators that read mutable state widens it and requires the consumer to track that state.

**Actor Scope and Policy.** Scope grants are enforced after authentication and fail closed. The policy gate and the value restriction are enforced by each consumer (see [Actor Policies](#actor-policies)).

**Policy Target as Trust Anchor.** For a policy-bearing actor, the manager is fully trusted to enforce the committed limits; a buggy or malicious manager can do anything its own authority over the account allows. Accounts SHOULD point restricted keys only at audited managers. A manager SHOULD be non-upgradeable: an upgradeable manager lets whoever controls the upgrade rewrite enforcement. Checking that presented parameters match the stored commitment is the manager's responsibility.

**Policy State on Revocation.** `revokeActor` clears `actor_config`, `policy_commitment`, and `policy_manager`, immediately stopping the key from reaching its manager. Parameters a manager keeps in its own storage are not auto-cleared; wallets uninstall them through the manager, and an `expiry` bounds the window otherwise.

**Actor Management.** Config change authorization requires an admin actor. The EOA self-actor is implicitly admin and revocable via a config change. All actor modification paths are blocked while the account is locked.

**Actor Expiry.** Prefer non-expiring admins and apply `expiry` only to restricted keys; a sole expiring admin bricks the account and breaks cross-chain reconstruction.

**Local Epoch and Outstanding Signatures.** `IncrementLocalEpoch` cancels every unlanded local signature at the prior epoch, including unsequenced batches, but does not revoke live actors or affect the multichain channel. Wallets MUST treat an epoch increment, not mere non-inclusion, as the durable retirement of a JIT authorization.

**Implicit EOA Rule Scoping.** The self-actor rule only applies when authentication used the native secp256k1 path and `DEFAULT_EOA_REVOKED` is unset. Generic authenticator contracts MUST NOT satisfy it even if they return the account's own actorId; otherwise an arbitrary authenticator could authenticate as any EOA.

**Revocation Does Not Disable the Key Elsewhere.** Setting `DEFAULT_EOA_REVOKED` removes the EOA key's authority in every Keystore consumer, but the key can still sign legacy transactions for the account. Accounts that must not be controlled by a secp256k1 key should be created through account creation rather than rely on revocation.

**actorId Binding.** Consumers check that the authenticator's returned `actorId` maps back to that authenticator in `actor_config`, preventing a malicious authenticator from claiming another authenticator's actors.

**Account Creation.** Initial actors are salt-committed, preventing front-running of actor assignment. Wallet bytecode should be inert when uninitialized, since it can be permissionlessly deployed. Creation applies only to addresses that satisfy CREATE2 freshness.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
