---
eip: 8130
title: Account Abstraction by Account Configuration
description: Enable account abstraction feature set through onchain account configurations and a new transaction type
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, 2718
---

## Abstract

This proposal introduces a new [EIP-2718](./eip-2718.md) transaction type and an onchain Account Configuration system that together provide account abstraction: custom authentication, call batching, and gas sponsorship. Accounts register actors with onchain authenticator contracts. Transactions declare which authenticator to use, enabling nodes to filter transactions without executing arbitrary wallet code. No EVM changes are required. The contract infrastructure is designed to be shared across chains as a common base layer for account management.

## 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 transaction explicitly declares its authenticator, a contract that takes a hash and signature data and returns the authenticated actor. This makes validation predictable: wallets know the rules, and nodes can see exactly what computation a transaction requires before executing it. Instead of simulating arbitrary code, nodes filter on authenticator identity, accepting transactions whose authenticator belongs to a small, standard canonical set and rejecting the rest.

New signature algorithms are introduced through authenticator contracts and standardized through the canonical authenticator set.

## Specification

### Overview

An account authorizes one or more **actors** which are credentials permitted to act on its behalf. Each actor is bound to an **authenticator**, an onchain contract that checks a signature and returns the actor's identity (`actorId`). An account's actors, their authenticators, and their permissions are held in the **Account Configuration Contract** at `ACCOUNT_CONFIG_ADDRESS`.

A new [EIP-2718](./eip-2718.md) transaction type (`AA_TX_TYPE`) names the authenticator that authenticates it. Because the authenticator is declared explicitly, a node can tell exactly what computation a transaction requires, and reject unknown authenticators before executing any code. Validation reads the account's configuration, runs the named authenticator, and checks the resolved actor's permissions; execution then dispatches the transaction's calls.

The specification is organized around four pieces:

- **Actors and Authenticators**: the authentication and authorization model (see [Authenticators](#authenticators)).
- **Account Configuration**: onchain management of account and its actors (see [Account Configuration](#account-configuration)).
- **AA Transaction Type**: the wire format, signature payloads, and gas accounting (see [AA Transaction Type](#aa-transaction-type)).
- **Account Changes**: how accounts are created and how actors are added, revoked, or delegated (see [Account Changes](#account-changes)).

Accounts are portable across EVM chains; see [Portability](#portability).

#### Account Types

This proposal supports three paths for accounts to use AA transactions:

| Account Type | How It Works | Key Recovery |
|--------------|--------------|--------------|
| **EOAs** | EOAs send AA transactions using their existing secp256k1 key via native ecrecover. If the account has no code, the protocol auto-delegates to `DEFAULT_ACCOUNT_ADDRESS` (see [Block Execution](#block-execution)). Accounts MAY override with a delegation entry in `account_changes` or a standard [EIP-7702](./eip-7702.md) transaction | Wallet-defined; EOA recoverable via standard transactions |
| **Existing Smart Contracts** | Already-deployed accounts (e.g., ERC-4337 wallets) register actors via `importAccount()` on the Account Configuration Contract | Wallet-defined |
| **New Accounts (No EOA)** | Created via a create entry in `account_changes` with CREATE2 address derivation; runtime bytecode placed at address, actors + authenticators configured, `calls` handles initialization | Wallet-defined |

### Authenticators

Each actor is associated with an authenticator, a contract that performs signature authentication. In the protocol's terms, the authenticator *authenticates* the actor (it returns the actor's `actorId`); scope and policy then *authorize* what that authenticated actor may do. The authenticator address is stored in `actor_config` (see [Account Configuration](#account-configuration)). All authenticators implement `IAuthenticator.authenticate(hash, data)`. After the authenticator authenticates the signature, the protocol validates the returned `actorId` against `actor_config` and authorizes the actor by checking its scope against the authorization context; the policy gate (`POLICY`) is enforced later, during execution.

Authenticators are executed via STATICCALL. Authenticator addresses MUST NOT be delegated accounts; reject if the code at the authenticator address starts with the delegation indicator (`0xef0100`).

Chains choose how authenticator execution is priced. A chain MAY meter authenticator execution as ordinary EVM execution (see [Mempool Acceptance](#mempool-acceptance) for rules), or it MAY enshrine canonical authenticators and charge a fixed, standard gas cost per enshrined authenticator instead of metering. When an authenticator is enshrined, its execution MUST produce identical results to the corresponding authenticator contract.

`K1_AUTHENTICATOR` (`address(1)`) is a protocol-reserved address for native secp256k1 authentication. When the protocol encounters this address as an authenticator in auth data, it performs ecrecover directly rather than making a STATICCALL. The `data` portion is interpreted as raw ECDSA `(r || s || v)`, and the returned `actorId` is `bytes32(bytes20(recovered_address))`. The same identity serves both the implicit default EOA and any explicitly registered k1 actor; the `actor_config` slot alone distinguishes a full-owner EOA from a scoped key, so actors can be explicitly registered with `K1_AUTHENTICATOR` to use native ecrecover with a custom scope, without requiring a deployed authenticator contract. `address(0)` is considered empty.

Any contract implementing `IAuthenticator` can be permissionlessly deployed and registered as an actor's authenticator. However, registration does not make an authenticator usable on the 8130 path: only canonical authenticators in the node allowlist are accepted for block-level AA authentication (see [Canonical Authenticator Set](#canonical-authenticator-set)). Non-canonical authenticators remain fully usable within EVM execution; for example, an account can authenticate an actor against an arbitrary `IAuthenticator` via a config change call, enabling use cases such as wallet-defined recovery methods. Such actors simply cannot authenticate transactions directly over the 8130 path and they operate through ordinary EVM execution.

#### Canonical Authenticator Set

This specification defines a canonical authenticator set which is the set of signature algorithms that compliant nodes MUST accept. The initial canonical set includes:

| Name | Algorithm | Authenticator | `actorId` Derivation |
|------|-----------|----------|----------------------|
| k1 | secp256k1 | `K1_AUTHENTICATOR` (native sentinel) | `bytes32(bytes20(recovered_address))` |
| 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(bytes20(delegated_address))`: signatures from `delegated_address` are valid for the registering account (see [Delegate Authenticator](#delegate-authenticator)) |

The canonical authenticator set and corresponding contract addresses are maintained in a companion ERC (number TBD) and deployed at deterministic CREATE2 addresses across chains. The canonical set is expected to grow as new algorithms are adopted (e.g., post-quantum) through the companion ERC process.

Nodes MUST include all canonical authenticators in their allowlist and SHOULD NOT extend the allowlist with non-canonical authenticators. The 8130 path is intended to use a small, standard set of signature algorithms; accepting additional authenticators is a divergence from this specification.

#### Delegate Authenticator

The delegate authenticator lets one account act on behalf of another. An account **A** registers a *delegate actor* that points at another account **B**; thereafter any key that can authenticate as **B** may authenticate as **A**, bounded by the scope **A** grants that delegate actor.

**Registration.** **A** authorizes an actor with `authenticator = DELEGATE_AUTHENTICATOR` and `actorId = bytes32(bytes20(B))`. That actor's `scope` in **A**'s config governs what **B** may do for **A** under the normal [Actor Scope](#actor-scope) rules.

**Wire format.** When authenticating through the delegate authenticator, the auth blob's `data` (the bytes following the 20-byte authenticator address, per [Signature Format](#signature-format)) is:

```
data = delegated_account (20 bytes)   // B
    || nested_auth                     // authenticator (20 bytes) || data, a normal auth blob for B
```

**Constraints**:

- **No nesting (depth-1):** the nested authenticator MUST NOT be the delegate authenticator; delegation cannot chain.
- **Nested authenticator MUST be canonical:** nodes apply the authenticator allowlist to the nested authenticator just as to the outer one, keeping total work bounded and enshrinable.
- **Admin on the nested actor:** the nested check is **B** vouching via a signature, and signing authority is the admin predicate (`scope == 0x00`), so the nested actor in **B** MUST be admin.

### Account Configuration

Each account can authorize a set of actors through the Account Configuration Contract at `ACCOUNT_CONFIG_ADDRESS`. This contract handles actor authorization, account creation, change sequencing, and delegates signature authentication to onchain [Authenticators](#authenticators).

Actors are identified by their `actorId`, a 32-byte identifier derived by the authenticator from public key material. Each authenticator defines its own actorId derivation algorithm (see [Canonical Authenticator Set](#canonical-authenticator-set)). Actors can be modified via calls within EVM execution by calling the authenticated config change functions.

#### Storage Layout

Each actor occupies a single `actor_config` slot containing the authenticator address, scope byte, and optional expiry. When `scope` includes `POLICY` (`0x02`), the actor also carries a signed policy commitment and a manager address in the separate policy slots `policy_commitment`/`policy_manager` (see [Actor Policies](#actor-policies)). Actors are revoked by deleting the `actor_config` slot. The self-actor (`actorId == bytes32(bytes20(account))`) is the one exception; its layout and rules are collected in [Self-Actor](#self-actor).

| Field | Bytes | Description |
|-------|-------|-------------|
| `authenticator` | 0–19 | Authenticator contract address |
| `scope` | 20 | Permission bitmask (`0x00` = unrestricted; also the admin predicate, see [Actor Scope](#actor-scope)) |
| `expiry` | 21–26 | `uint48` Unix timestamp (seconds); the actor is invalid once `block.timestamp > expiry`. `0` = no expiry |
| reserved | 27–31 | MUST be zero on write (native and EVM paths). Implicit version discriminator: new-format configs with nonzero reserved fail to apply through legacy deployments rather than applying with restrictions dropped |

When `scope & POLICY != 0`, the actor's policy is held in two additional slots:

```
policy_commitment(account, actorId) → bytes32   // set when POLICY is set
policy_manager(account, actorId)    → address   // set when POLICY is set
```

These slots are read only during execution (see [Actor Policies](#actor-policies)); validity is still decided by the single `actor_config` SLOAD.

#### Self-Actor

The **self-actor** is the actor whose `actorId == bytes32(bytes20(account))`. All self-actor rules — referenced from Storage Layout, Validation, `account_changes_cost`, Execution, `importAccount`, and Security Considerations — are collected here:

- **Two mutually exclusive forms.** The self-actor exists in exactly one of two forms at a time, and registering one clears the other:
  1. **Inline secp256k1 self (default EOA).** Held **inline in the packed account-state slot** (`default_eoa_scope`/`default_eoa_expiry` plus the `DEFAULT_EOA_REVOKED` flag; see [Account Lock](#account-lock)), so the account's own key resolves in a single SLOAD, including the scoped-self case. Authenticated via native ecrecover, never an external authenticator contract.
  2. **Non-secp256k1 self authenticator.** Held in the `actor_config(self)` slot (e.g. an authenticator that returns the self-actorId). Registering it **sets** `DEFAULT_EOA_REVOKED`, disabling the inline k1 self.
- **Implicit EOA authorization.** A native secp256k1 signature recovering to the account authenticates as the self-actor whenever `DEFAULT_EOA_REVOKED` is not set, resolved from the inline default-EOA config. For a fresh account that inline config is all-zero: `scope == 0x00`, `expiry == 0` (non-expiring admin). This lets every existing EOA send AA transactions immediately without prior registration. (Scoping restriction: [Implicit EOA Rule Scoping](#security-considerations).)
- **Scoped/disabled inline self.** Registering the self with `K1_AUTHENTICATOR` (`address(1)`) sets a custom `scope`/`expiry` while retaining native ecrecover: it writes the inline default-EOA fields (a non-zero `scope` downgrades the key) and leaves `DEFAULT_EOA_REVOKED` clear. Setting `DEFAULT_EOA_REVOKED` disables the inline secp256k1 self in its entirety.
- **Revocation.** Revoking the self-actor writes the account-state slot (`DEFAULT_EOA_REVOKED`) and leaves the `actor_config(self)` slot empty and reusable.
- **Defaults.** `createAccount` and `importAccount` set `DEFAULT_EOA_REVOKED` by default, so a newly created or imported account does not leave a native secp256k1 owner live unless one is among `initial_actors` (a quantum-safe default).

#### Actor Scope

The scope byte in `actor_config` is a 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 — a permanently inert actor until a protocol change defines those bits.

| Bit | Value | Name | Context |
|-----|-------|------|---------|
| 0 | `0x01` | SENDER | Ungated initiation: `sender_auth`; may originate transactions to any `call.to`. Carries operational authority (unless combined with `POLICY`) — also satisfies `verifySignature()` (ERC-1271 signing); see below |
| 1 | `0x02` | POLICY | Gated initiation: `sender_auth`; may originate transactions only to the actor's `policy_manager` (see [Actor Policies](#actor-policies)) |
| 2 | `0x04` | NONCE | Permits a restricted actor to use sequenced `nonce_key`s for sender-context transactions (see [Actor Nonce Scope](#actor-nonce-scope)) |
| 3 | `0x08` | SELF_PAYER | Self-pay gas: authorizes paying the account's own gas when `payer == sender` |
| 4 | `0x10` | SPONSOR_PAYER | Sponsor gas: authorizes acting as `payer_auth` for a different sender (`payer != sender`) |
| 5–7 | | (spare) | Reserved for future pure grants |

**Operational authority.** An actor is *operational* when it can originate calls to any target: `operational := admin (scope == 0x00) || (SENDER && !POLICY)`. ERC-1271 signing (`verifySignature()`) is **not** a grant; it is authorized for any operational actor. Signing is an *encoding* of authority, not a separate capability: an operational key can already call `approve`/`transfer` directly, so letting it produce a `Permit` or order signature grants nothing it did not already have (`approve ≡ permit`). Conversely a `POLICY` actor is never operational and MUST NOT sign raw hashes — a signature acts off the policy gate, so a gated key that could sign would escape its gate. A sign-only restriction would be illusory anyway (a hash-blind signer can still sign a Permit2 drain), so signing gets no bit; scoped signing is expressed at the account layer via an approved-hash / approve-typed-data pattern driven by a `POLICY` key (see [Actor Policies](#actor-policies)).

**Admin.** Config-change authority (`authorizeActor`, `revokeActor`, `applySignedActorChanges`, delegation) is exactly the admin predicate `scope == 0x00`: the same unrestricted root every other check already special-cases. 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. See [Why Admin Is Scope Zero](#why-admin-is-scope-zero).

**Initiation grants.** `SENDER` and `POLICY` compose: `SENDER` allows initiation to any `call.to`, `POLICY` allows gated initiation to the actor's `policy_manager`. An actor may hold either or both; whenever `POLICY` is set the call is gated to the `manager` regardless of `SENDER`, so the gate always binds a policy-bearing actor. When both are set, `SENDER` conveys no additional authority; wallets SHOULD NOT set `SENDER | POLICY`. `POLICY | SELF_PAYER` and `POLICY | NONCE` compose likewise. `POLICY | SPONSOR_PAYER` also composes: the actor's initiation is gated to its `manager`, but its sponsor authority is **not** gated and it may underwrite third-party gas off its policy gate. `authorizeActor` stores any scope combination verbatim; combination semantics are checked at the point of use.

**Verbatim reporting.** `getActorConfig` and `authenticateActor` MUST return stored scope bytes unmodified — no normalization or masking of unrecognized bits.

Scope authorization is applied only after an authenticator authenticates the signature and returns an `actorId`: the protocol loads that actor's `scope` and checks it against the context being authorized. For `sender_auth`, require `scope == 0x00 || (scope & SENDER) != 0 || (scope & POLICY) != 0`; when `POLICY` is set the actor is gated to its `manager` regardless of `SENDER` (see [Actor Policies](#actor-policies)). For self-pay (payer account == sender account) require SELF_PAYER on the actor that authorizes payment, and for sponsorship (payer account ≠ sender) require SPONSOR_PAYER on the `payer_auth`-resolved actor (the respective bit, or unrestricted); for `verifySignature()` require **operational** (`scope == 0x00`, or `SENDER` and not `POLICY`); for config change `auth` require admin (`scope == 0x00`).

The protocol validates signatures by reading `actor_config` directly and delegating authentication to [Authenticators](#authenticators); see [Validation](#validation) for the full flow. Actor enumeration is performed off-chain via `ActorAuthorized` and `ActorRevoked` event logs.

#### Actor Nonce Scope

`NONCE` (`0x04`) grants a restricted actor access to ordered (sequenced) `nonce_key`s for **sender-context** transactions.

| Actor | `nonce_key` allowed |
|-------|----------------------|
| Admin (`scope == 0x00`) | Any `nonce_key`, including `NONCE_KEY_MAX` (nonceless), freely |
| Restricted, `NONCE` unset | `nonce_key == NONCE_KEY_MAX` only (nonceless) |
| Restricted, `NONCE` set | Any `nonce_key`, including `NONCE_KEY_MAX` — the full 2D key space |

This makes nonceless (`NONCE_KEY_MAX`) the default for every restricted (non-admin) actor: a freshly authorized session key can send transactions without touching a nonce channel. An actor that needs an ordered, replay-protected sequence (e.g. a high-throughput automation key) opts in with `NONCE` and may then use any `nonce_key` in the full 2D space.

`NONCE` constraints apply to **sender-context** transactions only. `payer_auth` consumes no payer-side sequence and is unaffected. The check (reject a sequenced `nonce_key` from a restricted actor lacking `NONCE`) is enforced by the protocol; see [Validation](#validation).

#### Actor Expiry

The protocol enforces one rule: an actor is **live** at `now` (the inclusion block timestamp) iff `expiry == 0 || now <= expiry` (see [Storage Layout](#storage-layout)), evaluated at inclusion. It checks only the acting key's own `expiry` and never walks authorizer lineage, 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, where a lapsed key simply stops working. Using it on an admin (`scope == 0x00`) can break valid change chains for cross-chain actor-change replay and account sync, so an admin SHOULD NOT expire unless the wallet owner deliberately plans for it and keeps a non-expiring owner in place.

#### 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 (see [Why Actor Policies?](#why-actor-policies)).

`POLICY` selects whether an actor is gated; gating is determined by the scope bit alone. When set, `policyData` MUST be exactly `manager` (20 bytes) ‖ `commitment` (32 bytes), written verbatim to `policy_manager` / `policy_commitment`; neither field need be nonzero. A zero `commitment` is a valid "no parameters" value. A zero `manager` gates the key to `address(0)`, leaving no productive call target. When unset, `policyData` MUST be empty and prior policy slots are cleared. Policy vocabulary lives in the commitment and manager logic.

| `POLICY` | Gate: `call.to` MUST equal | `policyData` at authorize |
|----------------|----------------------------|---------------------------|
| unset | (no gate) | empty |
| set | the actor's `manager` (`address(0)` = no productive target) | `manager` (20) ‖ `commitment` (32) |

A policy-bearing actor may call exactly one target: its configured `manager`. The contract at that target reads the actor's `commitment` (via [`getPolicy`](#iaccountconfiguration)), validates presented parameters against it, enforces *what* the call may do, and carries out the approved action. The protocol's only responsibility is the single-target gate.

A key that should be enforced by the account's own code rather than a separate contract sets `manager = account`. Because protocol dispatch originates the call from the account itself (`msg.sender == account`), 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 (see [Security Considerations](#security-considerations)).

**Scope.** `POLICY` is the gated initiation grant. How it composes with `SENDER` is defined in [Actor Scope](#actor-scope). It MAY combine with `SELF_PAYER` so a session key can self-pay gas (bounded by balance and `expiry`). It MAY also combine with `NONCE` so a policy key can use a sequenced `nonce_key`. A `POLICY` key is not operational and cannot sign ERC-1271 (a signature would act off its gate; see [Actor Scope](#actor-scope)); scoped signing SHOULD use an account-level approved-hash / approve-typed-data pattern (Safe `approveHash` precedent) driven by the `POLICY` key.

**Reference flow** (non-normative). One construction of a session-key policy:

1. **Authorize.** The account authorizes the key with `POLICY` (optionally `| SELF_PAYER` and/or `| NONCE`), the `manager`, and a `commitment`. The key's call authority is only the gate to its `manager`.
2. **Install (once).** The `params` are installed at the `manager`, permissionlessly, gated by the signed commitment.
3. **Use (each call).** The protocol gate routes the key's call to the `manager`; the manager enforces installed `params` and drives the account.
4. **Retire.** `revokeActor` zeroes the commitment; `expiry` rejects further authentication.

**Example** (non-normative). A subscription session key limited to **5 USDC per 30-day period** with a two-target allowlist. Authorized with `POLICY` (nonceless by default, and without `SELF_PAYER` if a sponsor pays gas). Over-budget transfers, wrong targets, expiry, or revoke all fail.

**The commitment is signed, opaque, and protocol-stored.** One signature fully describes the key's manager and policy and travels with the portable actor-change path.

**Enforcement.** The gate is applied during [Call Execution](#call-execution). Validity does not read `policy_commitment` or `policy_manager`.

**Lifecycle.** Policy state is keyed by `(account, actorId)`; clearing on revocation and target-held parameters are covered under [Policy State on Revocation](#security-considerations).

#### 2D Nonce Storage

Nonce state is managed by a precompile at `NONCE_MANAGER_ADDRESS`. The protocol reads and increments nonce slots directly during AA transaction processing; the precompile exposes a read-only `getNonce()` interface to the EVM.

The transaction carries two nonce fields: `nonce_key` (`uint256`) selects the nonce channel, and `nonce_sequence` (`uint64`) is the expected sequence number within that channel.

| `nonce_key` Range | Name | Description |
|-------------------|------|-------------|
| `0` | Standard | Sequential ordering, mempool default |
| `1` through `NONCE_KEY_MAX - 1` | User-defined | Parallel transaction channels defined by wallets |
| `NONCE_KEY_MAX` | Nonce-free | No nonce state read or incremented |

##### Nonce-Free Mode (`NONCE_KEY_MAX`)

When `nonce_key == NONCE_KEY_MAX`, the protocol does not read or increment the nonce counter. `nonce_sequence` MUST be `0`. Replay protection relies on `expiry`, which MUST be non-zero, together with `replay_id` deduplication state distinct from the nonce counter. That state is a fixed-capacity **circular buffer** and is **consensus state**, not per-node bookkeeping: a `seen` map (`replay_id → expiry`) holds live entries, and a ring of `replay_id`s in insertion order lets the oldest entry be evicted once expired. Per transaction the protocol reads `seen[replay_id]` and rejects if a still-live entry exists; reads the ring slot at the current pointer; if that slot is occupied, reads that entry's expiry and, when expired, clears its `seen` entry (rejecting only if the buffer is full of still-live entries); then writes the new `replay_id` into the ring slot, sets `seen[replay_id] = expiry`, and advances the pointer. Because the `seen[replay_id]` liveness check and the full-buffer rejection both decide block validity, the buffer capacity MUST be a **protocol constant or explicit chain parameter** (`REPLAY_BUFFER_CAPACITY`), identical for every node on the chain — a per-node capacity would split consensus. It MUST be at least `peak accepted nonce-free throughput × NONCE_FREE_EXPIRY_WINDOW`, so an entry always expires before its ring slot is reused. Because entries are ephemeral and the buffer is fixed-size, there is no permanent state growth. See `nonce_key_cost` in [Intrinsic Gas](#intrinsic-gas).

The maximum `expiry` window accepted for nonce-free transactions is likewise an explicit chain parameter (`NONCE_FREE_EXPIRY_WINDOW`), not a per-node choice, and MUST be sized together with `REPLAY_BUFFER_CAPACITY` so `peak accepted nonce-free throughput × NONCE_FREE_EXPIRY_WINDOW` stays within capacity. Replay protection is handled by the **replay identifier** defined below.

###### Replay Identifier

Nonce-free (`NONCE_KEY_MAX`) transactions have no nonce slot to key deduplication and replacement on, so each carries a **replay identifier** that names the *logical* transaction independent of its fees and authorization blobs. Standard and 2D transactions (`nonce_key != NONCE_KEY_MAX`) do **not** use `replay_id`; they are deduplicated and replaced by `(sender, nonce_key, nonce_sequence)` under the standard nonce rules (see [Mempool Replacement](#mempool-replacement)).

```
REPLAY_ID_TYPE = 0x7901

replay_id = keccak256(REPLAY_ID_TYPE || rlp([
  chain_id, resolved_sender, expiry,
  account_changes, calls, metadata,
  payer
]))
```

`nonce_key` and `nonce_sequence` are omitted: a nonce-free transaction always has `(NONCE_KEY_MAX, 0)`, so they carry no entropy and cannot distinguish two logical transactions.

`resolved_sender` is the sender address recovered via ecrecover (EOA path, where `sender` is empty in the wire format) or taken directly from the `sender` field (configured-actor path). Binding `resolved_sender` keeps the identifier unique per sender on the EOA path, where two distinct EOAs can sign identical transaction bodies.

`replay_id` deliberately excludes:

- `max_fee_per_gas`, `max_priority_fee_per_gas`, and `gas_limit`, so a fee bump on an otherwise-identical transaction does not change its identity.
- `sender_auth` and `payer_auth`, the authorization blobs, which are excluded both because they are non-deterministic (ECDSA signing is randomized and malleable) and because `payer_auth` can be freshly re-signed (e.g. on a fee bump) without changing the logical transaction.

It includes `payer` (the address field, not `payer_auth`): retargeting a transaction at a different payer is a change to the logical transaction, so it MUST produce a different `replay_id`.

The full transaction hash MUST NOT be used for nonce-free deduplication or mempool replacement (see [Mempool Replacement](#mempool-replacement) below); the rationale for keying on `replay_id` instead is explained in [Security Considerations](#security-considerations).

###### Mempool Replacement

Fee-bump replacement is keyed differently depending on nonce mode:

- **Standard and 2D transactions** (`nonce_key != NONCE_KEY_MAX`): replacement follows the standard nonce rules. Two pending transactions from the same `sender` sharing the same `(sender, nonce_key, nonce_sequence)` are **replacement candidates**; `replay_id` plays no role. Nonce mechanics already prevent two transactions with the same `(sender, nonce_key, nonce_sequence)` from both being included, since inclusion increments the sequence.
- **Nonce-free transactions** (`nonce_key == NONCE_KEY_MAX`): `replay_id` drives replacement because there is no nonce slot. Two pending transactions from the same `sender` with the same `replay_id` are **replacement candidates**, and block builders MUST NOT include two transactions with the same `(sender, replay_id)` in the same block.

In both modes a replacement MUST increase `max_priority_fee_per_gas` by at least the node's configured minimum bump (e.g., ≥10%), mirroring standard replace-by-fee conventions, and MUST be independently fully valid, including a fresh `payer_auth` when sponsored: `payer_auth` commits to `max_fee_per_gas`, `max_priority_fee_per_gas`, and `gas_limit` (see [Signature Payload](#signature-payload)), so the payer must re-sign to authorize the bumped fees. For nonce-free transactions, changing `payer` yields a new `replay_id` (a new logical transaction) rather than a replacement of the old one.

#### Account Lock

Account lock state is stored in a single packed 32-byte account-state slot that also holds the change sequences, an account-flags byte, and the inline default-EOA (self-actor) config:

| Field | Description |
|-------|-------------|
| `multichain_sequence` | Change-sequence counter for `chain_id 0` (`uint64`) |
| `local_sequence` | Change-sequence counter for the local chain (`uint64`); `> 0` doubles as the initialized flag |
| `flags` | Account flags byte: bit 0 (`DEFAULT_EOA_REVOKED`) disables the secp256k1 self-actor; bit 1 (`LOCKED`) freezes actor configuration; bit 2 (`UNLOCK_INITIATED`) selects how the `lock_union` field is interpreted |
| `lock_union` | `uint40` union field. While `UNLOCK_INITIATED` is clear it holds `unlock_delay` (seconds, `uint16` range): the notice required before config can change, which nodes read for rate-limit tiering. While `UNLOCK_INITIATED` is set it holds `unlocks_at` (the timestamp at which unlock takes effect) |
| `default_eoa_scope` | Inline self-actor `scope` (`uint8`; `0x00` = full owner) |
| `default_eoa_expiry` | Inline self-actor `expiry` (`uint48` Unix seconds; `0` = no expiry) |
| reserved | Remaining bytes in the packed slot; MUST be zero |

The packed slot is exactly 32 bytes, so the inline self-actor config and lock state cost no extra SLOAD/SSTORE beyond the account-state access already performed for the change-sequence fields.

When `LOCKED`, all actor-config changes and delegation are rejected on both paths (config entries in `account_changes` and `applySignedActorChanges()`). The only operation permitted while locked is `unlock` below. 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 self-bricking config rotations.

##### Lock Operations

Lock state changes only through `applySignedLockChanges`, a dedicated admin-authorized entry point accessible in the EVM only.

```
LOCK_CHANGE_TYPEHASH = keccak256(
  "SignedLockChange(address account,uint256 chainId,uint8 op,"
  "uint16 unlockDelay,uint64 sequence)")
// op: 1 = lock, 2 = unlock

applySignedLockChanges(address account, uint8 op,
                       uint16 unlockDelay, bytes calldata auth)
```

Lock operates using the local account channel. The digest binds `chainId = block.chainid` and `sequence = local_sequence` (the **current** counter value); the contract rejects a mismatch and, on success, increments `local_sequence` — the same sign-current-then-increment convention as transaction nonces and local config changes (which share this counter). `auth` is a standard `authenticator \|\| data` blob (see [Signature Format](#signature-format)) validated as **admin** (`scope == 0x00`) against `account`. Anyone may relay; authorization comes from the signature.

**Lifecycle** — `lock`, then `unlock`, with no other actions in between:

1. **Lock** (`op = 1`): only from the unlocked state. Sets `LOCKED` and stores `unlock_delay = unlockDelay`. Rejected if already locked; the delay cannot be changed while locked.
2. **Unlock** (`op = 2`): only from `LOCKED` with no pending unlock; `unlockDelay` MUST be `0`. Sets `UNLOCK_INITIATED` and `unlocks_at = block.timestamp + unlock_delay` (from the stored delay).
3. **Effective unlock**: once `block.timestamp >= unlocks_at`, the account is unlocked and config changes resume; the flags and `lock_union` are lazily cleared by the next op. Locking again requires a fresh `lock`.

#### Account Import

`importAccount(address account, uint256 chainId, InitialActor[] calldata initialActors, bytes calldata signature)` is a one-time call that registers an already-deployed account into the Account Configuration Contract with an initial actor set. `chainId` is the replay domain of the import signature, mirroring `applySignedActorChanges`: `0` = multichain (valid on every chain); otherwise it MUST equal `block.chainid`. The call is rejected when:

- `chainId` is neither `0` nor the current chain.
- The account already has 8130 state: **either** change-sequence channel is non-zero. Import is a one-time bootstrap, so it requires both the local and multichain channels empty. A locked account is always caught here: a `lock` is a signed local config change, so it has advanced `local_sequence` and the account is considered initialized (see [Account Lock](#account-lock)).

The `signature` is validated against the account via [ERC-1271](./eip-1271.md) `isValidSignature(digest, signature)`, binding the initial actor set to the account's existing authorization logic. `digest` is a typed `ActorInitialization` struct hash:

```
ACTORCONFIG_TYPEHASH =
  keccak256("ActorConfig(address authenticator,uint8 scope,uint48 expiry)")

ACTOR_TYPEHASH =
  keccak256("Actor(bytes32 actorId,ActorConfig config,bytes policyData)"
            "ActorConfig(address authenticator,uint8 scope,uint48 expiry)")

ACTOR_INITIALIZATION_TYPEHASH =
  keccak256("ActorInitialization(bytes32 salt,uint256 chainId,Actor[] initialActors)"
            "Actor(bytes32 actorId,ActorConfig config,bytes policyData)"
            "ActorConfig(address authenticator,uint8 scope,uint48 expiry)")

// Per-actor. Imported actors carry scope and policyData like create; expiry is always 0
// (expiry is added post-import via config changes). policyData follows authorizeActor's rule:
// manager (20) || commitment (32) when POLICY is set, empty otherwise, and is hashed for real:
configHash_i = keccak256(abi.encode(ACTORCONFIG_TYPEHASH, authenticator_i, scope_i, 0))
actorHash_i  = keccak256(abi.encode(ACTOR_TYPEHASH, actorId_i, configHash_i, keccak256(policyData_i)))

digest = keccak256(abi.encode(
    ACTOR_INITIALIZATION_TYPEHASH,
    bytes32(bytes20(account)),                               // salt, bound to the account address
    chainId,
    keccak256(abi.encodePacked(actorHash_0, ..., actorHash_n))
))
```

This digest is a typed (EIP-712-style) struct hash that intentionally omits the EIP-712 domain separator, which prevents phishing via standard wallet signing flows. The `salt` field is bound to the account address, and `chainId` binds the replay domain (matching `applySignedActorChanges`). This typed style is distinct from the packed style used for [Address Derivation](#address-derivation); that split is intentional. Imported actors are always non-expiring: implementations MUST hash `expiry = 0` into every `configHash_i` (as shown), and MUST NOT accept an actor-provided expiry at import. `policyData` is validated with `authorizeActor`'s frozen rule (well-formed `manager ‖ commitment` when `POLICY` is set, empty otherwise), written to `policy_manager`/`policy_commitment`, and hashed into `actorHash_i`; unlike create, `manager = account` is expressible here.

On success, `importAccount` sets the `DEFAULT_EOA_REVOKED` flag (parity with `createAccount`), disabling the implicit native-secp256k1 owner. An owner who wants to keep using that key past import includes the self-actorId as a `K1_AUTHENTICATOR` entry in `initialActors` (lossless: still a full owner, now resolved through its inline default-EOA config, which clears the flag for the self).

#### Delegation Indicator

The delegation indicator is the [EIP-7702](./eip-7702.md) mechanism for pointing an account's code at a shared implementation contract. This proposal relies on it as the foundation for account code: an EOA sending its first AA transaction is auto-delegated to `DEFAULT_ACCOUNT_ADDRESS` when it has no code (see [Block Execution](#block-execution)), and accounts MAY set or clear delegation explicitly via a [Delegation Entry](#delegation-entry) in `account_changes`. Because 8130 depends on this behavior directly, the delegation indicator MUST be supported on 8130 chains even when standalone [EIP-7702](./eip-7702.md) transactions are not enabled.

An account is delegated when its code is exactly `0xef0100 || target`, where `target` is a 20-byte address. Delegated accounts MAY originate transactions, and all code-executing operations targeting a delegated account MUST load code from `target` instead of the indicator.

`DEFAULT_ACCOUNT_ADDRESS` SHOULD implement [ERC-1271](./eip-1271.md) by delegating to the Account Configuration Contract's `verifySignature()`, and SHOULD implement token receiver hooks (ERC-721, ERC-1155) to safely receive assets.

### AA Transaction Type

A new [EIP-2718](./eip-2718.md) transaction with type `AA_TX_TYPE`:

```
AA_TX_TYPE || rlp([
  chain_id,
  sender,             // Sender address (20 bytes) | empty for EOA signature
  nonce_key,          // uint256: nonce channel selector
  nonce_sequence,     // uint64: sequence number
  expiry,             // Unix timestamp (seconds)
  max_priority_fee_per_gas,
  max_fee_per_gas,
  gas_limit,
  account_changes,    // Account creation, config change, and/or delegation operations | empty
  calls,              // [[call, ...], ...] where call = rlp([to, data]) | empty
  metadata,           // opaque attribution/annotation bytes | empty
  payer,              // empty = sender-paid, payer_address = specific payer
  sender_auth,        // raw ECDSA r||s||v (EOA, sender empty) | authenticator || data (configured actor)
  payer_auth          // empty = self-pay | authenticator || data = sponsored (same format as sender_auth)
])

call = rlp([to, data])   // to: address, data: bytes
```

#### Field Definitions

| Field | Description |
|-------|-------------|
| `chain_id` | Chain ID per [EIP-155](./eip-155.md) |
| `sender` | Sending account address. **Required** (non-empty) for configured actor signatures. **Empty** for EOA signatures; address recovered via ecrecover. The presence or absence of `sender` is the sole distinguisher between EOA and configured actor signatures. |
| `nonce_key` | `uint256` nonce channel selector. `0` for standard sequential ordering, `1` through `NONCE_KEY_MAX - 1` for parallel channels, `NONCE_KEY_MAX` for nonce-free mode. |
| `nonce_sequence` | `uint64` expected sequence number within `nonce_key`. Must match current sequence for `(sender, nonce_key)`. Incremented after inclusion regardless of execution outcome. Must be `0` when `nonce_key == NONCE_KEY_MAX`. |
| `expiry` | `uint64` Unix timestamp (seconds since epoch); encoded as a canonical minimal-length RLP integer. Transaction invalid when `block.timestamp > expiry`. A value of `0` means no expiry. Must be non-zero when `nonce_key == NONCE_KEY_MAX`. |
| `max_priority_fee_per_gas` | Priority fee per gas unit ([EIP-1559](./eip-1559.md)) |
| `max_fee_per_gas` | Maximum fee per gas unit ([EIP-1559](./eip-1559.md)) |
| `gas_limit` | Maximum gas budget for sender-intrinsic gas (intrinsic gas excluding payer authentication) and call execution (see [Intrinsic Gas](#intrinsic-gas)). Payer authentication is metered separately and does not draw from `gas_limit` |
| `account_changes` | **Empty**: No account changes. **Non-empty**: Array of typed entries: create (type `0x00`) for account deployment, config change (type `0x01`) for actor management, and delegation (type `0x02`) for code delegation. See [Account Changes](#account-changes) |
| `calls` | **Empty**: No calls. **Non-empty**: Array of call phases; see [Call Execution](#call-execution) |
| `metadata` | **Empty**: No metadata. **Non-empty**: Opaque attribution or annotation bytes. See [Transaction Metadata](#transaction-metadata) |
| `payer` | Gas payer identity. **Empty**: Sender pays. **20-byte address**: This specific payer required. See [Payer Modes](#payer-modes) |
| `sender_auth` | See [Signature Format](#signature-format) |
| `payer_auth` | Payer authorization. **Empty**: self-pay. **Non-empty**: `authenticator \|\| data`, same format as `sender_auth`. See [Payer Modes](#payer-modes) |

#### Intrinsic Gas

**Intrinsic gas** follows the standard Ethereum meaning: the total cost to include the transaction. As in standard transactions it accounts for signature authentication, here both the sender's authentication (`sender_auth_cost`) and the payer's (`payer_auth_cost`) are included.

The specific per-component gas *values* in this section are a **recommended schedule** that reflects the EVM access and data-availability costs ([EIP-2929](./eip-2929.md) / [EIP-2028](./eip-2028.md)) at the time of writing. They are a reference, not protocol constants: just as a chain may choose how it prices authenticator execution (see [Authenticators](#authenticators)), a chain MAY adopt a different intrinsic-gas schedule, for example to track a future EVM repricing or a local cost model. The formula's structure (its components and what each one accounts for) is the normative part; the absolute numbers below are the recommended values for a chain that mirrors current EVM costs.

```
intrinsic_gas = AA_BASE_COST + tx_payload_cost + nonce_key_cost + bytecode_cost + account_changes_cost + auto_delegation_cost + sender_auth_cost + payer_auth_cost
```

**Sender-intrinsic gas** is intrinsic gas excluding payer authentication and is bounded by `gas_limit`. For self-pay transactions `payer_auth_cost` is `0`, so sender-intrinsic gas equals intrinsic gas:

```
sender_intrinsic_gas = intrinsic_gas - payer_auth_cost
                     = AA_BASE_COST + tx_payload_cost + nonce_key_cost + bytecode_cost + account_changes_cost + auto_delegation_cost + sender_auth_cost
```

Intrinsic gas is charged before `calls` run. Since `payer_auth_cost` is metered outside `gas_limit`, the gas available to `calls` is:

```
execution_gas_available = gas_limit - sender_intrinsic_gas
```

`payer_auth_cost` is metered separately and charged to the payer on top of `gas_limit`; it does not draw from `gas_limit` and cannot reduce the gas available to `calls`. This isolation is required: `payer_auth` is excluded from both the sender and payer signature hashes (see [Signature Payload](#signature-payload)) and is selected unilaterally by the payer. If it consumed `gas_limit`, a payer could choose an expensive authenticator to starve `calls` and alter execution behavior. Keeping it separate makes the gas available to `calls` a function of sender-signed fields alone. The payer reimburses `payer_auth_cost` regardless (the payer pays all gas). Note this means gas used can be greater than gas limit when not self-pay. 

`sender_auth_cost`, by contrast, is included in `gas_limit`: the sender chooses its own authenticator, and both sender and payer sign over `gas_limit`, so both commit to the full sender-side budget before inclusion.

**`sender_auth_cost`**: authenticator execution + 1 SLOAD (`actor_config`).

**`payer_auth_cost`**: 0 for self-pay (`payer` empty). Otherwise, the same `sender_auth_cost` model applies to the payer's authenticator.

| Component | Value |
|-----------|-------|
| `AA_BASE_COST` | 15,000 gas: the fixed per-transaction overhead of the AA path, analogous to the standard 21,000 base cost but excluding the components broken out separately below (signature authentication is `sender_auth_cost`/`payer_auth_cost`; calldata is `tx_payload_cost`). It covers transaction decoding, sender/actor resolution and the `actor_config` access accounting common to every AA transaction, nonce-mode dispatch, and receipt assembly. Like the other values it is a recommended figure a chain MAY reprice |
| `tx_payload_cost` | Standard per-byte cost over the entire RLP-serialized transaction: 16 gas per non-zero byte, 4 gas per zero byte, consistent with [EIP-2028](./eip-2028.md). Ensures all transaction fields (`account_changes`, `sender_auth`, `calls`, `metadata`, etc.) are charged for data availability |
| `nonce_key_cost` | `NONCE_KEY_MAX`: 13,000 gas (ring-buffer replay state: 2 cold SLOADs + 1 warm SLOAD + 3 warm SSTORE resets; the ring pointer's SLOAD/SSTORE are amortized across the block). Otherwise: 22,100 gas for first use of a `nonce_key` (cold SLOAD + SSTORE set), 5,000 gas for existing keys (cold SLOAD + warm SSTORE reset) |
| `bytecode_cost` | 0 if no create entry in `account_changes`. Otherwise: 32,000 (deployment base) + code deposit cost (200 gas per deployed byte). Byte costs for `code` are covered by `tx_payload_cost`; the create entry's initial-actor slot writes are covered by `account_changes_cost` |
| `account_changes_cost` | Per applied create entry: one `actor_config` slot write per initial actor (22,100 gas each: cold SLOAD + SSTORE set). When an initial actor sets `POLICY`, its `policy_manager` and `policy_commitment` slots are also written (22,100 gas each), so a POLICY initial actor is 3 slot-sets (~66,300 gas) versus 1 (~22,100) for a non-policy actor; the extra `policyData` bytes are charged through `tx_payload_cost`. Expiry is not expressible at create. Per applied config change entry: auth authentication cost (same model as `sender_auth_cost`) + storage write costs for each mutated actor slot (`actor_config`; plus `policy_commitment` and `policy_manager` when `(scope & POLICY) != 0`). A config change that authorizes or revokes the **self-actor** mutates the packed account-state slot rather than an `actor_config` slot (the inline default-EOA `scope`/`expiry` and the `DEFAULT_EOA_REVOKED` bit) and, for the mutual-exclusion check between the inline secp256k1 self and a non-k1 self, additionally accesses the reserved `actor_config(self)` slot: +2,100 (cold SLOAD) for a `K1_AUTHENTICATOR` self change, or the normal `actor_config` write plus the account-state slot write for a non-secp256k1 self change. Per applied delegation entry: delegation indicator deposit (4,600 gas, 200 × 23 bytes). Per skipped config change entry (already applied): 2,100 (SLOAD to check sequence). 0 if no create, config change, or delegation entries in `account_changes` |
| `auto_delegation_cost` | Delegation indicator deposit: 4,600 gas (200 × 23 bytes for the `0xef0100 \|\| address` indicator) when a code-less `sender` is auto-delegated to `DEFAULT_ACCOUNT_ADDRESS` (Block Execution step 4). 0 otherwise. |

#### Signature Format

Signature format is determined by the `sender` field:

**EOA signature** (`sender` empty): Raw 65-byte ECDSA signature `(r || s || v)`. The sender address is recovered via ecrecover.

**Configured actor signature** (`sender` set):

```
authenticator (20 bytes) || data
```

The first 20 bytes identify the authenticator address. When the authenticator is `K1_AUTHENTICATOR`, `data` is raw ECDSA `(r || s || v)` and the protocol handles ecrecover natively. For all other authenticators, `data` is authenticator-specific; each authenticator defines its own wire format.

##### Validation

1. **Resolve sender**: If `sender` empty, ecrecover derives the sender address (EOA path) with `actorId = bytes32(bytes20(sender))`. If `sender` set, read the first 20 bytes of `sender_auth` as the authenticator address.
2. **Authenticate**: Route by authenticator address. For the EOA path (`sender` empty), ecrecover was already performed in step 1. For `K1_AUTHENTICATOR` (`address(1)`), the protocol natively ecrecovers from `data` (as `r || s || v`), returning `actorId = bytes32(bytes20(recovered_address))`. For all other authenticators, call `authenticator.authenticate(hash, data)` via STATICCALL, returning `actorId` (or `bytes32(0)` for invalid). `address(0)` is never a valid authenticator selector (it is the empty `actor_config` sentinel).
3. **Authorize**: **Self-actor (native secp256k1) rule**: if authentication in step 2 used the native secp256k1 path (the EOA path or `K1_AUTHENTICATOR`) and `actorId == bytes32(bytes20(sender))`, resolve the self-actor from the inline default-EOA 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 full owner, i.e. admin). Otherwise SLOAD `actor_config(sender, actorId)`, reject if its reserved bytes are nonzero (version gate, see [Storage Layout](#storage-layout)), and require that the stored authenticator address matches the effective authenticator (this covers every other actor, including a non-secp256k1 self). In either case, if the resolved `expiry` is non-zero, also require `block.timestamp <= expiry`; an expired actor is rejected.
4. **Check scope**: Read the resolved `scope` byte (from the inline default-EOA config for the secp256k1 self-actor, or `actor_config` otherwise) and check it against the context being authorized per [Actor Scope](#actor-scope) (for `sender_auth`: `scope == 0x00 || (scope & (SENDER | POLICY)) != 0`, with `POLICY` gating the actor to its `manager`). Payer scope is checked when the payer is resolved (see [Validation Flow](#validation-flow) step 6).
5. **Check nonce scope** (sender-context only, when `nonce_key != NONCE_KEY_MAX`): require the resolved actor be admin or carry `NONCE`, per [Actor Nonce Scope](#actor-nonce-scope).

#### Signature Payload

Sender and payer use different type bytes for domain separation, preventing signature reuse attacks:

**Sender signature hash**, all tx fields through `payer`, excluding `sender_auth` and `payer_auth`:

```
keccak256(AA_TX_TYPE || rlp([
  chain_id, sender, nonce_key, nonce_sequence, expiry,
  max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
  account_changes, calls, metadata,
  payer
]))
```

**Payer signature hash**, all tx fields through `payer`, excluding `sender_auth` and `payer_auth`:

```
keccak256(AA_PAYER_TYPE || rlp([
  chain_id, sender, nonce_key, nonce_sequence, expiry,
  max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
  account_changes, calls, metadata,
  payer
]))
```

The `sender` field in the payer signature hash MUST be the resolved sender address. In the EOA path (`sender` empty in the transaction wire format), the recovered sender address (from `sender_auth` ecrecover, see [Validation](#validation) step 1) MUST be substituted into the `sender` position before computing this hash; it MUST NOT be encoded as the empty wire-format value. This binds the payer's signature to the specific resolved sender and prevents cross-sender replay of payer signatures (see [Payer Security](#security-considerations)). The `payer` field MUST also be included so the payer's signature is bound to the account being charged; without it, a payer authorization could be redirected to any other account that authorizes the same actor with SPONSOR_PAYER scope (e.g., via the [delegate authenticator](#delegate-authenticator)).

#### Payer Modes

Gas payment and sponsorship are controlled by two independent fields:

**`payer`**, the sender's commitment regarding the gas payer, included in the sender's signed hash:

| Value | Mode | Description |
|-------|------|-------------|
| empty | Self-pay | Sender pays their own gas |
| `payer_address` (20 bytes) | Sponsored | Sender binds tx to a specific sponsor |

**`payer_auth`** uses the same `authenticator || data` format as `sender_auth`:

| `payer` | `payer_auth` | Payer Address | Validation |
|---------|-------------|---------------|------------|
| empty | empty | `sender` | Self-pay: no separate `payer_auth`; the resolved sender actor MUST have SELF_PAYER scope |
| `sender` address | `authenticator (20) \|\| data` | `sender` | Self-pay via a dedicated gas key: payer account == sender. Reads the `payer_auth`-resolved actor on the account, which MUST have SELF_PAYER scope (lets a SELF_PAYER-only key fund another sender key's transactions on the same account) |
| other address | `authenticator (20) \|\| data` | `payer` field | Sponsored (payer != sender): any authenticator. Reads payer's `actor_config`, validates against `payer` address, and requires SPONSOR_PAYER scope |


### Transaction Metadata

The `metadata` field is optional opaque bytes for attaching attribution or annotation data to a transaction, for example builder/app attribution, a payment reference or memo, or a commitment to off-chain data. Legacy transactions carry such data as a "data suffix" appended to `tx.input`; because [calls](#call-phases) replace the single input blob, `metadata` provides the equivalent home.

`metadata` is part of the signed transaction (covered by both the [sender and payer signature payloads](#signature-payload)) and is charged per byte through `tx_payload_cost` like any other transaction bytes. It does not affect validation or execution. Off-chain consumers (indexers, explorers) read it directly from the transaction. Producers MAY use any encoding.

### Account Changes

The `account_changes` field is an array of typed entries for account creation and actor management:

| Type | Name | Description |
|------|------|-------------|
| `0x00` | Create | Deploy a new account with initial actors (must be first, at most one) |
| `0x01` | Config change | Actor management: authorizeActor, revokeActor |
| `0x02` | Delegation | Set code delegation via the delegation indicator (at most one per transaction) |

Create and delegation entries are authorized by the transaction's `sender_auth` and there is no separate authorization field. The initial `actorId`s for create entries are salt-committed to the derived address. Delegation requires the sender to be the account's implicit EOA actor and admin (`scope == 0x00`). Config change entries carry their own `auth` and use a sequence counter for deterministic cross-chain ordering. Nodes SHOULD enforce a configurable per-transaction limit on the number of config change entries (mempool rule).

#### Create Entry

New smart contract accounts can be created with pre-configured actors in a single transaction. The `code` is placed directly at the account address, it is not executed during deployment. The account's initialization logic runs via `calls` in the execution phase that follows:

```
rlp([
  0x00,               // type: create
  user_salt,          // bytes32: User-chosen uniqueness factor
  code,               // bytes: Runtime bytecode placed at account address
  initial_actors      // Array of [actorId, authenticator, scope, policyData] tuples
])
```

Each initial actor carries an `actorId`, `authenticator`, `scope` (`uint8`; `0x00` = unrestricted admin), and `policyData` (empty, or `manager ‖ commitment` when `POLICY` is set), all committed to the derived address (see [Address Derivation](#address-derivation)) so the counterfactual address binds each initial actor's authority. Two things are **not** expressible here: `expiry`, and a self-referential `manager = account` (the account address is not yet known at commitment time). Both are added post-creation with a config change entry, which MAY accompany the create entry in the same `account_changes` array for atomic setup and, running after creation, resolves `manager = account` to a concrete address. For example, a subaccount is created with an admin actor (a delegate to the primary account, `scope = 0x00`) and a restricted app key (`SENDER | SELF_PAYER`, or `POLICY` with an external `manager` inline, or `manager = account` via an accompanying config change).

##### Address Derivation

Addresses are derived using the CREATE2 address formula with the Account Configuration Contract (`ACCOUNT_CONFIG_ADDRESS`) as the deployer. The `initial_actors` MUST be provided already sorted by `actorId` in strictly ascending order. Requiring a single canonical ordering keeps address derivation deterministic (a given set of actors always produces the same address), and the strict ordering also rejects duplicate `actorId`s:

```
// initial_actors MUST already be sorted by actorId (strictly ascending); reject otherwise

actors_commitment = keccak256(
    actorId_0 || authenticator_0 || scope_0 || policyData_0 ||
    ...
    actorId_n || authenticator_n || scope_n || policyData_n
)

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

The per-actor contribution is `actorId || authenticator || scope || policyData`: the 32-byte `actorId`, 20-byte `authenticator`, 1-byte `scope`, and the `policyData` bytes (empty when `POLICY` is unset, or exactly 52 bytes — `manager (20) ‖ commitment (32)` — when `POLICY` is set). The per-actor length is fully determined by `scope`, so the concatenation remains unambiguous. Expiry does not participate. The required strictly-ascending `actorId` ordering makes the commitment canonical.

`DEPLOYMENT_HEADER(n)` is a fixed 14-byte EVM loader that returns the trailing code (see [Appendix: Deployment Header](#appendix-deployment-header) for the full opcode sequence). On non-8130 chains, `createAccount()` constructs `deployment_code` and passes it as init_code to CREATE2. On 8130 chains, the protocol constructs the same `deployment_code` for address derivation but places `code` directly. Callers only provide `code`, the header is never user-facing.

##### Validation (Create Entry)

When a create entry is present in `account_changes`:

1. Parse `[0x00, user_salt, code, initial_actors]` where each entry is `[actorId, authenticator, scope, policyData]`. `scope` is stored verbatim (unknown scope bits allowed, per [Actor Scope](#actor-scope)). Apply `authorizeActor`'s frozen `policyData` rule: reject unless `policyData` is exactly `manager (20) ‖ commitment (32)` when `scope & POLICY != 0`, or empty otherwise. `expiry` is not accepted in the create entry
2. Require `initial_actors` are sorted by `actorId` in strictly ascending order; reject any unsorted set (strict ascending order also rejects duplicate `actorId` values).
3. Reject if `code` is empty or `len(code) > MAX_CODE_SIZE` ([EIP-170](./eip-170.md): 24576 bytes), to keep the placed code within the EVM contract size limit that CREATE/CREATE2 would otherwise enforce
4. Use `initial_actors` in the provided (sorted) order
5. Compute `actors_commitment` per [Address Derivation](#address-derivation)
6. Compute `effective_salt = keccak256(user_salt || actors_commitment)`
7. Compute `deployment_code = DEPLOYMENT_HEADER(len(code)) || code`
8. Compute `expected = keccak256(0xff || ACCOUNT_CONFIG_ADDRESS || effective_salt || keccak256(deployment_code))[12:]`
9. Require `sender == expected`
10. Require the destination matches CREATE2 freshness: `code_size(sender) == 0` and `nonce(sender) == 0` (matching the conditions under which CREATE2 would be permitted to deploy)
11. Validate `sender_auth` against one of `initial_actors` (actorId resolved from auth must match an entry's actorId and the auth authenticator must match that entry's authenticator)

#### Config Change Entry

Config change entries manage the account's actors. Each entry includes a `chain_id` field where `0` means valid on any chain, allowing replay across chains to synchronize actor state.

##### Config Change Format

```
rlp([
  0x01,               // type: config change
  chain_id,           // integer (EIP-155); 0 = valid on any chain. Hashed as uint256 in the signed digest
  sequence,           // uint64: monotonic ordering
  actor_changes,      // Array of actor changes
  auth                // Signature from an actor valid at this sequence
])

actor_change = rlp([
  change_type,          // uint8: operation type (see below)
  actorId,              // bytes32: actor identifier
  data                  // bytes: operation-specific, ABI-encoded (see below)
])
```

The operation-specific `data` is an opaque `bytes` blob carried in the RLP envelope but encoded with the contract ABI, so the same blob is decoded identically whether the change is applied natively or via `applySignedActorChanges()` on the Account Configuration Contract. It is also the value hashed (as `keccak256(data)`) in the [Config Change Signature Payload](#config-change-signature-payload).

**Operation types**:

| change_type | Name | `data` | Description |
|-------------|------|--------|-------------|
| `0x01` | `authorizeActor` | `abi.encode(ActorConfig config, bytes policyData)` (see [`ActorConfig`](#iaccountconfiguration)) | Authorize a new actor. Writes `actor_config` with `authenticator`, `scope`, and `expiry` (`0` = no expiry) verbatim (unknown scope bits stored as-is). If `POLICY` is set, requires `policyData = manager ‖ commitment` (exactly 52 bytes; neither field need be nonzero) and writes those slots; otherwise requires empty `policyData` and clears policy slots. Writes the packed `actor_config` word with its reserved bytes zeroed (they are not a caller input; see [Storage Layout](#storage-layout)). Does **not** enforce scope-combination exclusivity (that is use-time / protocol). Emits `ActorAuthorized` whose `actorData` is 32 bytes (`authenticator ‖ scope ‖ expiry ‖ reserved`) when `POLICY` is unset, or 84 bytes (appending `manager ‖ commitment`) when `POLICY` is set. |
| `0x02` | `revokeActor` | empty (`0x`) | Revoke an existing actor. Deletes `actor_config` (and `policy_commitment`/`policy_manager`). For the implicit EOA actor (`actorId == bytes32(bytes20(account))`), instead sets the account's `DEFAULT_EOA_REVOKED` flag bit (no `actor_config` write) to prevent implicit re-authorization. Emits `ActorRevoked`. |

#### Config Change Authorization

Each config change entry represents a set of operations authorized at a specific sequence number. The `auth` must be valid against the account's actor configuration *at the point after all previous entries in the list have been applied*. The authorizing actor must be **admin**: `scope == 0x00` (see [Actor Scope](#actor-scope)).

The sequence number is scoped by `chain_id`: `0` uses the multichain sequence channel (valid on any chain), while a specific `chain_id` uses that chain's local channel.

The change-sequence channels double as the initialized flag. Creation and import set the local channel to `1`, and any applied config change bumps whichever channel it used (local for a chain-specific `chain_id`, multichain for `chain_id 0`). Otherwise only the implicit EOA is on the account. 

##### Config Change Signature Payload

Entry signatures use ABI-encoded type hashing. Operations within an entry are individually ABI-encoded and hashed into an array digest:

```
TYPEHASH = keccak256("SignedActorChanges(address account,uint256 chainId,uint64 sequence,ActorChange[] actorChanges)ActorChange(uint8 changeType,bytes32 actorId,bytes data)")
ACTORCHANGE_TYPEHASH = keccak256("ActorChange(uint8 changeType,bytes32 actorId,bytes data)")

actorChangeHashes = [keccak256(abi.encode(ACTORCHANGE_TYPEHASH, changeType, actorId, keccak256(data))) for each actorChange]
actorChangesHash = keccak256(abi.encodePacked(actorChangeHashes))

digest = keccak256(abi.encode(TYPEHASH, account, chainId, sequence, actorChangesHash))
```

Domain separation from transaction signatures (`AA_TX_TYPE`, `AA_PAYER_TYPE`) is structural; transaction hashes use `keccak256(type_byte || rlp([...]))`, which cannot produce the same prefix as `abi.encode(TYPEHASH, ...)`.

The `auth` follows the same [Signature Format](#signature-format) as `sender_auth` (`authenticator || data`), validated against the account's actor state at that point in the sequence.

##### Account Config Change Paths

The same signed actor change can be applied through two paths:

- **`account_changes` (tx field)**: processed by the protocol before code deployment on 8130 chains.
- **`applySignedActorChanges()` (EVM)**: applied during EVM execution on any chain, including non-8130 chains and ERC-4337 deployments.

Both paths carry the same signed actor changes, share the same `change_sequence` counters, and are equally portable (`chain_id 0` for cross-chain or a specific `chain_id` for chain-local). They differ only in transport: the protocol consumes the signed change directly on 8130 chains, while everywhere else the EVM function does. `applySignedActorChanges()` parses the authenticator address from `auth`, calls the authenticator to get the `actorId`, and checks `actor_config`. `authorizeActor` writes `actor_config` and, when `POLICY` is set, the `policy_commitment` and `policy_manager` slots; `revokeActor` clears them all. Anyone can call these functions; authorization comes from the signed operation, not the caller. `authorizeActor`/`revokeActor` and delegation are blocked when the account is locked; lock/unlock use the dedicated `applySignedLockChanges` entry point (see [Account Lock](#account-lock)).

#### Delegation Entry

Delegation entries set [EIP-7702](./eip-7702.md)-style code delegation for the sender's account, replacing the need for an `authorization_list` in the transaction. Delegation is authorized by the transaction's `sender_auth`, no separate signature is required. The sender must have been **authenticated via the native secp256k1 path** (the EOA path or `K1_AUTHENTICATOR`; mirroring the [Implicit EOA Rule Scoping](#security-considerations)), with `actorId == bytes32(bytes20(sender))` and admin (`scope == 0x00`). A non-secp256k1 self authenticator that returns the self-actorId does not qualify, keeping delegation authority ECDSA/7702-portable.

##### Delegation Format

```
rlp([
  0x02,               // type: delegation
  target              // address: delegate to this contract, or address(0) to clear
])
```

The delegation is only permitted when:

- `code_size(sender) == 0` (empty account), or
- `code(sender)` starts with the delegation designator `0xef0100` (updating an existing delegation)

It will not replace non-delegation bytecode.

When `target` is `address(0)`, the delegation indicator is cleared and the account's code hash is reset to the empty code hash.

For 8130 transactions, successful delegation updates emit a protocol-injected `DelegationApplied(account, target)` receipt log, where `target` is the delegated contract address (or `address(0)` when clearing delegation).

#### Execution (Account Changes)

`account_changes` entries are processed in order before call execution:

1. **Create entry** (if present): Register `initial_actors` in Account Config storage for `sender`, for each `[actorId, authenticator, scope, policyData]` tuple writing `actor_config` with the given `authenticator` and `scope` verbatim and `expiry = 0` (expiry is not expressible at create). When `scope & POLICY != 0`, also write `policy_manager`/`policy_commitment` from `policyData` (`manager ‖ commitment`); when unset there are no policy slots. A tuple naming the self-actorId (`actorId == bytes32(bytes20(sender))`) with `K1_AUTHENTICATOR` writes its `scope` into the inline default-EOA fields instead (`expiry = 0`). Mark the account initialized by setting its local change-sequence channel to `1` (see [Config Change Authorization](#config-change-authorization)). Initialize lock state to safe defaults: `LOCKED`/`UNLOCK_INITIATED` flags clear, `lock_union = 0`. Set the `DEFAULT_EOA_REVOKED` flag so a freshly created account does not leave a native secp256k1 owner live unless one is among `initial_actors`. Place `code` at `sender`.
2. **Config change entries** (if any): Apply operations in entry order. Reject transaction if account is locked.
3. **Delegation entries** (if any): Require admin EOA-actor delegation authority (see [Delegation Entry](#delegation-entry)). Reject if account is locked. For each entry, set `code(sender) = 0xef0100 || target` (or clear if `target` is `address(0)`). Reject if account has non-delegation bytecode.

### Execution

#### Call Execution

The protocol dispatches calls directly from `sender` to each call's `to` address:

| Parameter | Value |
|-----------|-------|
| `from` (caller) | `sender` (the sender) |
| `to` | `call.to` |
| `tx.origin` | `sender` |
| `msg.sender` at target | `sender` |
| `msg.value` | 0 |
| `data` | `call.data` |

Calls carry no ETH value. ETH transfers are initiated by the account's wallet bytecode via the CALL opcode (see [Why No Value in Calls?](#why-no-value-in-calls)).

##### Call Phases

`calls` is a two-level structure: an ordered array of **phases**, where each phase is an ordered array of individual calls (`[[call, ...], [call, ...]]`). This gives two levels of atomicity where calls grouped within a phase are all-or-nothing, while phases commit independently in sequence (see [Why Call Phases?](#why-call-phases)).

Phases execute in order from a single gas pool (`gas_limit`). Within each phase, calls execute in order and are **atomic** so if any call in a phase reverts, all state changes for that phase are discarded and remaining phases are **skipped**. Completed phases **persist** and their state changes are committed and survive later phase reverts.

**Policy gate**: When the transaction's authenticating actor has `POLICY` set (and exclusivity checks passed), each call is gated before dispatch. The protocol resolves the actor's allowed target (`policy_manager(sender, actorId)`) once at the start of `calls` execution, and that snapshot gates every call in every phase; a config change applied by an earlier call does not retarget the gate for later calls. If `call.to` is not that address, the call is **not dispatched** and fails deterministically with the protocol revert `ActorPolicyViolation(bytes32 actorId, address target)`:

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

The frame's return data is `abi.encodeWithSelector(ActorPolicyViolation.selector, actorId, call.to)`. This is a consensus-level result, not a validity error, so standard atomicity applies: the enclosing phase's state changes roll back, later phases are skipped, and the phase is reported as failed in `phaseStatuses`. Only work already performed is charged (intrinsic gas plus the one `policy_manager` SLOAD); the undispatched call body costs nothing and the transaction is still included with its nonce consumed.

**Common patterns**:

- **Simple call**: `[[call]]`, one phase, one call (`call = rlp([to, data])`)
- **Atomic batch**: `[[call_a, call_b, call_c]]`, one phase, all-or-nothing
- **Sponsor + user**: `[[sponsor_payment], [user_action_a, user_action_b]]`, sponsor in phase 0 (committed), user actions in phase 1 (atomic, skipped if sponsor fails)

#### Transaction Context

The Transaction Context precompile at `TX_CONTEXT_ADDRESS` provides read-only access to the current AA transaction's metadata during call execution. The precompile is populated only while the protocol is dispatching the transaction's `calls`; validation and account-change processing do not populate it. It reads directly from the client's in-memory transaction state; protocol "writes" are effectively zero-cost. Gas is charged as a base cost plus 3 gas per 32 bytes of returned data, matching `CALLDATACOPY` pricing.

| Function | Returns | Available |
|----------|---------|-----------|
| `getTransactionSender()` | `address`, the account executing calls (`sender`) | Execution only |
| `getTransactionPayer()` | `address`, gas payer (`sender` for self-pay, payer for sponsored) | Execution only |
| `getTransactionSenderActorId()` | `bytes32`, authenticated actor's actorId | Execution only |

If the wallet needs the authenticator address or scope, it calls `getActorConfig(account, actorId)` on the Account Configuration Contract. A policy target reached as a `call.to` identifies which key it is acting for by combining `getTransactionSender()` and the authenticated `getTransactionSenderActorId()` from this precompile, then reads the actor's gate target and signed `commitment` via `getPolicy(account, actorId)` in one call and validates the presented policy parameters against the commitment. The commitment lives in Account Configuration storage (where it is written and revoked), not the precompile, keeping the precompile to immutable transaction context.

**Non-8130 chains**: No code at `TX_CONTEXT_ADDRESS`; STATICCALL returns zero/default values.

### Portability

The system is split into storage and authentication layers with different portability characteristics:

| Component | 8130 chains | Non-8130 chains |
|-----------|-------------|-----------------|
| **Account Configuration Contract** | Protocol reads storage directly for validation; EVM interface available | Standard contract (ERC-4337 compatible factory) |
| **Authenticator Contracts** | Protocol calls authenticators via STATICCALL | Same onchain contracts callable by account config contract and wallets |
| **Code Delegation** | Delegation entry in `account_changes` (EOA-only authorization in this version) | Standard [EIP-7702](./eip-7702.md) transactions (ECDSA authority) |
| **Transaction Context** | Precompile at `TX_CONTEXT_ADDRESS`; protocol populates, authenticators read | No code at address; STATICCALL returns zero/default values |
| **Nonce Manager** | Precompile at `NONCE_MANAGER_ADDRESS` | Not applicable; nonce management by existing systems (e.g., ERC-4337 EntryPoint) |

All contracts are deployed at deterministic CREATE2 addresses across chains.

### Validation Flow

#### Mempool Acceptance

1. Parse and structurally validate `sender_auth`. Verify `account_changes` contains at most one create entry (type `0x00`, must be first) and at most one delegation entry (type `0x02`). Nodes SHOULD enforce a configurable limit on the number of config change entries (type `0x01`).
2. Resolve sender: if `sender` set, use it; if empty, ecrecover from `sender_auth`
3. Determine effective actor state:
   a. If create entry present in `account_changes`: verify address derivation, `code_size(sender) == 0`, use `initial_actors`
   b. Else: read from Account Config storage
4. If config change or delegation entries present in `account_changes`: reject if account is locked (see [Account Lock](#account-lock)). For config change entries: simulate applying operations in sequence, skip already-applied entries. For delegation entries: verify `code_size(sender) == 0` or existing delegation designator.
5. Validate `sender_auth` against resulting actor state (see [Validation](#validation)) and check scope per [Actor Scope](#actor-scope): SENDER (or `POLICY`) for the sender context, and admin or `NONCE` when `nonce_key != NONCE_KEY_MAX`. Payer scope (SELF_PAYER / SPONSOR_PAYER) is checked in step 6, not here — the sender actor is not required to carry a payer bit. If delegation entries are present, the resolved actor must additionally hold admin EOA-actor delegation authority (see [Delegation Entry](#delegation-entry)).
6. Resolve payer from `payer` and `payer_auth`:
   - `payer` empty and `payer_auth` empty: self-pay. Payer is `sender`; the resolved sender actor's SELF_PAYER scope authorizes payment. Reject if balance insufficient.
   - `payer` = `sender` (explicit) with `payer_auth`: self-pay via a dedicated gas key. Payer account == sender; validate `payer_auth` against the account's `actor_config` and require SELF_PAYER scope on the resolved actor. Reject if balance insufficient.
   - `payer` = a different 20-byte address (sponsored): `payer_auth` uses any authenticator. Validate `payer_auth` against the `payer` address's `actor_config`. Require SPONSOR_PAYER scope on the resolved actor.
7. Verify nonce, payer ETH balance, and expiry. Two independent `expiry` fields are checked and both MUST be satisfied: the transaction `expiry` (inclusion validity, `block.timestamp <= expiry`) and the resolved actor's `expiry` (key liveness, per [Actor Expiry](#actor-expiry) and step 3). Either one lapsed rejects the transaction. Regardless of nonce mode, nodes MAY also reject a transaction whose `expiry` is too near to be reliably included.
   - **Standard keys** (`nonce_key != NONCE_KEY_MAX`): require `nonce_sequence == current_sequence(sender, nonce_key)`.
   - **Nonce-free key** (`nonce_key == NONCE_KEY_MAX`): skip nonce check, require `nonce_sequence == 0`, require non-zero `expiry`, and reject if `expiry` is farther out than `NONCE_FREE_EXPIRY_WINDOW` (the chain parameter bounded by `REPLAY_BUFFER_CAPACITY`, see [Nonce-Free Mode](#nonce-free-mode-nonce_key_max)). Deduplicate by the [Replay Identifier](#replay-identifier) (`replay_id`), not the full transaction hash.
8. Mempool threshold: gas payer's pending count below node-configured limits.
9. Apply [Mempool Replacement](#mempool-replacement) rules: for standard and 2D transactions (`nonce_key != NONCE_KEY_MAX`), when a pending transaction from the same `sender` shares this transaction's `(nonce_key, nonce_sequence)`; for nonce-free transactions (`nonce_key == NONCE_KEY_MAX`), when a pending transaction from the same `sender` shares this transaction's `replay_id`.

Nodes maintain an authenticator allowlist per the [Canonical Authenticator Set](#canonical-authenticator-set) rules.

Nodes MAY apply higher pending transaction rate limits based on account lock state. A node can cheaply read the packed account-state slot and grant the higher tier when the account is `LOCKED`, has **no pending unlock** (`UNLOCK_INITIATED` clear), and carries an `unlock_delay` at or above the node's threshold (e.g. ≥ 6 hours) — the actor set is then frozen for at least that window:

- **Locked sender**: A locked `sender` account has a stable signature if combined with a stateless authenticator. Nodes can safely allow a higher sender rate.
- **Locked payer with trusted bytecode**: A locked `payer` account whose bytecode is recognized (e.g. a canonical account implementation that restricts ETH movement while locked) provides an additional guarantee that ETH balance only decreases via gas fees. Nodes can safely allow a higher payer rate for such accounts, supporting high-throughput payer/sponsor services.

#### Block Execution

1. If `account_changes` contains config change or delegation entries, read lock state for `sender`. Reject transaction if account is locked. If delegation entries are present, require admin EOA-actor delegation authority (see [Delegation Entry](#delegation-entry)).
2. ETH gas deduction from payer (`sender` for self-pay). Transaction is invalid if payer has insufficient balance.
3. If `nonce_key != NONCE_KEY_MAX`, increment nonce in Nonce Manager storage for `(sender, nonce_key)`. If `nonce_key == NONCE_KEY_MAX`, skip (nonce-free mode).
4. If `code_size(sender) == 0` and no create entry and no delegation entry is present in `account_changes`, auto-delegate `sender` to `DEFAULT_ACCOUNT_ADDRESS` (set code to `0xef0100 || DEFAULT_ACCOUNT_ADDRESS`). This delegation persists. A delegation entry that clears the indicator (`target = address(0)`) returns the account to code-less state, but auto-delegation re-applies on its next transaction: a code-less account cannot permanently opt out of auto-delegation without placing non-delegation bytecode (e.g. via a create entry).
5. Process `account_changes` entries in order (see [Execution (Account Changes)](#execution-account-changes)).
6. Set transaction context on the Transaction Context precompile (sender, payer, actorId).
7. Execute `calls` per [Call Execution](#call-execution) semantics.

Unused gas from `gas_limit` is refunded to the payer. For step 5, the protocol SHOULD inject log entries into the transaction receipt (e.g., `ActorAuthorized`, `ActorRevoked`, `AccountCreated`, `DelegationApplied`) matching the events defined in the [IAccountConfiguration](#iaccountconfiguration) interface, following the protocol-injected log pattern established by [EIP-7708](./eip-7708.md). These protocol-injected logs are emitted only for 8130 transactions.

### RPC Extensions

**`eth_getTransactionCount`**: Extended with optional `nonceKey` parameter (`uint256`) to query 2D nonce channels. Reads from the Nonce Manager precompile at `NONCE_MANAGER_ADDRESS`.

**`eth_getTransactionReceipt`**: AA transaction receipts include:

- `payer` (address): Gas payer address (`sender` for self-pay, specified payer for sponsored).
- `status` (uint8): `0x01` = all phases succeeded (or `calls` was empty), `0x00` = one or more phases reverted. Existing tools checking `status == 1` remain correct for the success path. **`status == 0` does not imply "no state change."** Committed earlier phases (the sponsor pattern relies on this), auto-delegation, applied `account_changes`, nonce consumption, and gas payment all persist through a later phase revert. Consumers MUST NOT treat a failed AA receipt as a no-op. `gasUsed` includes `payer_auth_cost` (see [Intrinsic Gas](#intrinsic-gas)).
- `phaseStatuses` (uint8[]): Per-phase status array. Each entry is `0x01` (success) or `0x00` (reverted). Phases after a revert are not executed and reported as `0x00`. Empty if `calls` was empty.

**`eth_estimateGas`** / **`eth_call`**: Accept the AA transaction fields (`sender`, `nonceKey`, `accountChanges`, `calls`, `expiry`, `metadata`, `payer`, `senderAuth`, `payerAuth`) alongside the standard request object and price the request as an `AA_TX_TYPE` transaction. Because authentication gas is determined by the auth blob's *shape* rather than a valid signature (see [Intrinsic Gas](#intrinsic-gas)), the request is priced from an **unsigned** representative blob; no signature is produced or verified. The sender is taken from `sender` or the standard `from` (equivalently) and if both are present they MUST be equal. 

### Constants

| Name | Value | Comment |
|------|-------|---------|
| `AA_TX_TYPE` | `0x79` | [EIP-2718](./eip-2718.md) transaction type |
| `AA_PAYER_TYPE` | `0x7A` | Magic byte for payer signature domain separation |
| `REPLAY_ID_TYPE` | `0x7901` | Magic prefix for `replay_id` domain separation (see [Replay Identifier](#replay-identifier)) |
| `AA_BASE_COST` | 15000 | Base intrinsic gas cost |
| `ACCOUNT_CONFIG_ADDRESS` | CREATE2-derived (resolved at deployment) | Account Configuration system contract address |
| `K1_AUTHENTICATOR` | `address(1)` | Native secp256k1 (ECDSA) authenticator (implicit default EOA and explicitly registered k1 actors) |
| `DEFAULT_EOA_REVOKED` | `0x01` | Account-state `flags` bit that disables the implicit default-EOA path |
| `LOCKED` | `0x02` | Account-state `flags` bit that freezes actor configuration (see [Account Lock](#account-lock)) |
| `UNLOCK_INITIATED` | `0x04` | Account-state `flags` bit selecting the `lock_union` interpretation (`unlock_delay` vs `unlocks_at`) |
| `NONCE_MANAGER_ADDRESS` | `0x813000000000000000000000000000000000aa01` | Nonce Manager precompile address |
| `TX_CONTEXT_ADDRESS` | `0x813000000000000000000000000000000000aa02` | Transaction Context precompile address |
| `DEFAULT_ACCOUNT_ADDRESS` | CREATE2-derived (resolved at deployment) | Default wallet implementation for auto-delegation |
| `NONCE_KEY_MAX` | `2^256 - 1` | Nonce-free mode (expiry-only replay protection) |
| `REPLAY_BUFFER_CAPACITY` | chain parameter | Fixed capacity of the nonce-free `replay_id` ring buffer; identical for every node on the chain (consensus). See [Nonce-Free Mode](#nonce-free-mode-nonce_key_max) |

### Appendix: Storage Layout

The protocol reads storage directly from the Account Configuration Contract (`ACCOUNT_CONFIG_ADDRESS`). The storage layout is defined by the deployed contract bytecode; slot derivation follows from the contract's Solidity storage declarations. The final deployed contract source serves as the canonical reference for slot locations.

### Appendix: Deployment Header

The `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)
]
```

The create entry only supports runtime bytecode. Delegation is set via delegation entries (type `0x02`) in `account_changes`.

## Rationale

### Why Authenticator Contracts?

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

### Why a Canonical Authenticator Set?

Without a required authenticator set, nodes could diverge on which signature algorithms they accept beyond `K1_AUTHENTICATOR`. Wallets would face a fragmented network where each node accepts a different combination of algorithms, making it impossible to guarantee transaction delivery for non-k1 signature types.

The canonical set establishes a shared baseline where wallets that use canonical authenticators know their transactions will be accepted by any compliant node. The set is expected to remain small, with new algorithms added through the companion ERC process as they gain broad adoption.

### Why Actor Policies?

Session keys often need narrow authority: only this token, only this much per day, only this action. `POLICY` makes gated initiation a first-class grant, distinct from ungated `SENDER`: the key may originate transactions, but only to a single call target, bound to a signed opaque **commitment**. The protocol's only policy responsibility is the single-target gate plus storing the commitment. Allowing `POLICY | SELF_PAYER` lets a session key self-pay; note this exposes the account's full ETH balance to gas spend (see [Why Split PAYER into SELF_PAYER and SPONSOR_PAYER?](#why-split-payer-into-self_payer-and-sponsor_payer)).

### Why Admin Is Scope Zero

Admin is the predicate `scope == 0x00` rather than a dedicated grant bit because a "config-only" grant would be indistinguishable from full access. Any key that can rewrite `actor_config` can grant itself `scope == 0x00`, so the authority to change configuration is already administratively equivalent to unrestricted authority. Naming admin as the absence of any restriction makes this self-escalation explicit instead of hiding it behind a bit that could be misread as narrowly scoped. It also gives every account a natural root: the all-zero inline default-EOA config is a non-expiring admin, and restricted keys are strictly grants below that root.

### Why No Public Key Storage?

Authenticators receive the public key (or full credential) in transaction calldata and recover the `actorId` from it, rather than the protocol storing keys onchain. This keeps per-actor state to a single `actor_config` SLOAD regardless of key size, which matters most for large post-quantum credentials: storing them would add permanent state growth of tens of slots per actor. Calldata is also cheaper than cold storage for material read once per transaction — on the order of 2,048 vs 6,300 gas for a P256 key and 21,000 vs 88,000 gas for a PQ key — so the calldata-plus-recovery model is both smaller and cheaper than an onchain key registry.

### Design Layering

Restrictions the Account Configuration contract must enforce are complete at deployment: the signer is admin (`scope == 0x00`), reserved-bytes-zero, and well-formed `policyData` when `POLICY` is set. Grants can grow because reads fail closed on unknown bits — `NONCE` itself is such a grant, added to the spare bit space without changing the frozen surface above. Signing semantics (e.g. approved typed data for session keys) evolve at the account layer.

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

The Account Configuration contract is a single immutable CREATE2 deployment, so it cannot know which grants or combinations future forks will define; rejecting combinations at write time would freeze today's rules into unupgradeable code. `authorizeActor` therefore stores scope verbatim and validates only timeless structure (admin signer, reserved-bytes-zero, well-formed `policyData`). Combination semantics are checked at the point of use by whichever context reads the scope — protocol validation for the transaction paths, and the contract's `verifySignature()` (operational) for the ERC-1271 path — so an unsatisfiable combo is simply inert.


### Why 2D Nonce + `NONCE_KEY_MAX`?

Additional `nonce_key` values allow parallel transaction lanes without nonce contention between independent workflows.

`NONCE_KEY_MAX` enables nonce-free transactions where replay protection comes from short-lived `expiry` and node-level deduplication by the fee- and signature-invariant [Replay Identifier](#replay-identifier). This is useful for operations where nonce ordering coordination is undesirable. Reserving `NONCE_KEY_MAX` as the sentinel permanently removes one channel from the `2^256`-wide nonce space, which is immaterial in practice; `NONCE_KEY_MAX` is always nonce-free and is never a sequenced channel, even for an actor holding `NONCE` scope.

### Why the NONCE Grant?

A restricted (non-admin) actor without `NONCE` is confined to `NONCE_KEY_MAX` (nonce-free), whose replay protection is a short `expiry` window (`NONCE_FREE_EXPIRY_WINDOW`): the transaction is invalid if not included in that window. An actor that instead needs an ordered, non-expiring sequence opts in with `NONCE` and may then use any `nonce_key` in the full 2D space. Making sequenced channels a grant, rather than the default, keeps the common session-key case coordination-free and reserves ordered channels for actors that explicitly want them.

### Why Split PAYER into SELF_PAYER and SPONSOR_PAYER?

Both authorize spending the account's ETH on gas, and neither bounds how much — a compromised key of either kind can burn the balance. The split separates what the spend can *buy*. `SELF_PAYER` converts ETH only into the account's own transactions (gated, if the actor carries `POLICY`); compromise is vandalism, monetizable only with a colluding builder taking priority fees. `SPONSOR_PAYER` converts ETH into inclusion for arbitrary third-party senders — an economically transferable authority that can be operated as a paymaster service and monetized out-of-band, no builder collusion required. Session keys get `SELF_PAYER` by default; a sponsorship service's hot key is `SPONSOR_PAYER`-only — the minimal grant for a paymaster operating from a locked treasury, with no `SENDER` or config authority. Paired with the locked-payer trusted-bytecode tier (see [Mempool Acceptance](#mempool-acceptance)), a node can reason "this key's only possible effect is a balance decrease via gas" from the scope byte alone. Because self-pay is defined relationally (`payer == sender`), the delegate-authenticator path threads through `SPONSOR_PAYER`: account B sponsoring via A's delegate actor is `payer != sender` from A's view and so requires `SPONSOR_PAYER` on that actor — default-deny for cross-account payment.

### Why Account Lock?

Locked accounts have a frozen actor set, so the primary state that can invalidate a validated transaction is nonce consumption. This can enable nodes to cache actor state and apply higher mempool rate limits (see [Mempool Acceptance](#mempool-acceptance)). A locked payer running a canonical account implementation that restricts ETH movement while locked further bounds balance changes to gas fees, letting nodes raise payer rate limits, useful for high-throughput payer/sponsor services. 

### Why CREATE2 for Account Creation?

The create entry uses the CREATE2 address formula with `ACCOUNT_CONFIG_ADDRESS` as the deployer address for cross-chain portability:

1. **Deterministic addresses**: Same `user_salt + code + initial_actors` produces the same address on any chain
2. **Pre-deployment funding**: Users can receive funds at counterfactual addresses before account creation
3. **Portability**: Same `deployment_code` produces the same address on both 8130 and non-8130 chains (see [Address Derivation](#address-derivation))
4. **Front-running prevention**: `initial_actors` in the salt prevents attackers from deploying with different actors (see [Create Entry](#create-entry))

### Why Delegation via Account Changes?

[EIP-7702](./eip-7702.md) introduced `authorization_list` as a transaction-level field for code delegation, with ECDSA authority. This proposal moves delegation into `account_changes`, authorized by the transaction's `sender_auth`. Delegation is restricted to senders authenticated via the native secp256k1 path (EOA path or `K1_AUTHENTICATOR`) with `actorId == bytes32(bytes20(sender))`, so that code delegation remains portable across non-8130 chains via standard [EIP-7702](./eip-7702.md) transactions (a non-secp256k1 self authenticator does not qualify, even if it returns the self-actorId). Eventually this can be expanded to all authenticator types, not just K1 EOAs. 

### External Account Factories

Account creation, import, signed actor changes, and locking are ordinary EVM entry points on the Account Configuration Contract, so external factories can compose them to mint accounts into a desired end state. This is always possible in EVM but is **not** available on the 8130 transaction path.

*Non-normative future work:* once [EIP-7819](./eip-7819.md) is adopted, the protocol could be extended with a single canonical external factory. The create entry would emit an [EIP-7702](./eip-7702.md) delegation prefix and deploy the account from that factory, which uses Account Configuration Contract state to check the signature and upgrade, extending the delegation account change to all authenticator types rather than the native secp256k1 EOA only as today.

### Why Call Phases?

Phases provide two atomic batching levels without per-call mode flags:

- **Atomic batching**: One phase, all-or-nothing.
- **Sponsor protection**: Payment in phase 0 persists even if user actions in phase 1 revert.

### Why a Metadata Field?

Attribution and annotation data (builder/app codes, payment references, off-chain commitments) traditionally ride as a trailing "data suffix" on `tx.input`. The structured `calls` array has no such trailing location, so a dedicated optional `metadata` field gives this data a first-class, signed home without overloading a call. Binding it into both signature payloads makes it tamper-evident.

### Why No Value in Calls?

The protocol dispatches each call directly to the specified `to` address with `msg.sender = sender`. Since every account has wallet bytecode (via auto-delegation or explicit deployment), ETH transfers route through wallet code via the CALL opcode; no capability is lost. Removing protocol-level value from calls means the protocol never moves ETH on behalf of the sender.

### Why a Transaction Context Precompile?

Transaction context (sender, payer, calls, gas) is immutable transaction metadata; it never changes during execution. `actorId` is set after validation and available during execution only. A precompile is the natural fit:

- **Zero protocol write cost**: The precompile reads directly from the client's in-memory transaction struct: no HashMap insert, no journaling, no rollback tracking.
- **Pull model**: Consumers read only what they need: policies read `actorId` to enforce per-actor limits, and accounts may inspect `payer` for fee reimbursement or access logic.
- **Forward compatible**: New context fields are added as new precompile functions, with no interface changes to `IAuthenticator` or existing authenticator contracts.

## Backwards Compatibility

No breaking changes. Existing EOAs and smart contracts function unchanged. Adoption is opt-in:

- EOAs continue sending standard transactions
- ERC-4337 infrastructure continues operating
- Accounts gain AA capabilities by configuring actors. EOAs sending their first AA transaction are auto-delegated to `DEFAULT_ACCOUNT_ADDRESS` if they have no code. EOAs MAY override with a delegation entry in `account_changes` (EOA-only authorization), a standard [EIP-7702](./eip-7702.md) transaction, or use a create entry in `account_changes` for custom wallet implementations

The `actor_config` layout (`authenticator ‖ scope ‖ expiry ‖ reserved`, with the scope-bit assignment and the reserved bytes acting as a version gate), the address-derivation commitment, and the ABI surface (typehashes, events, `importAccount`/`applySignedActorChanges` signatures) are defined by this specification. Because 8130 has no prior deployment, there is no live actor state written under an earlier format to migrate; the formats here are authoritative from first deployment.

## Reference Implementation

### IAccountConfiguration

The Account Configuration contract is the canonical ABI surface (no separate interface file is kept in sync). The
reference below mirrors the public structs, events, and functions of that contract.

```solidity
interface IAccountConfiguration {
    struct ChangeSequences {
        uint64 multichain; // chain_id 0
        uint64 local;      // chain_id == block.chainid; starts at 1 once initialized (created/imported), 0 = uninitialized
    }

    struct ActorConfig {
        address authenticator;
        uint8 scope;        // grants bitmask; 0x00 = unrestricted (admin); 0x01 = SENDER; 0x02 = POLICY; 0x04 = NONCE; 0x08 = SELF_PAYER; 0x10 = SPONSOR_PAYER; ERC-1271 signing requires operational authority (admin or SENDER without POLICY), not a grant
        uint48 expiry;      // Unix seconds; 0 = no expiry. Actor invalid once block.timestamp > expiry
    }

    // Actor used for account creation and import. Carries scope and policyData
    // (empty unless POLICY set, then manager[20] || commitment[32], per authorizeActor's rule).
    // expiry is NOT expressible here and is added post-deployment via config changes
    // (initial actors are always non-expiring).
    struct InitialActor {
        bytes32 actorId;
        address authenticator;
        uint8 scope;        // 0x00 = unrestricted (admin)
        bytes policyData;   // empty unless POLICY set; then manager[20] || commitment[32]
    }

    struct Actor {
        bytes32 actorId;
        ActorConfig config;
        bytes policyData;  // empty unless POLICY set; then manager[20] || commitment[32]
    }

    struct ActorChange {
        uint8 changeType;  // 0x01 = authorizeActor, 0x02 = revokeActor
        bytes32 actorId;
        bytes data;        // operation-specific: abi.encode(ActorConfig, bytes policyData) for authorize; empty for revoke
    }

    // Tightly packed authorization surface:
    //   authenticator(20) || scope(1) || expiry(6) || reserved(5) — 32 bytes —
    //   and, only when scope & POLICY != 0, manager(20) || commitment(32) (84 bytes total).
    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);
    // Protocol-injected receipt log for successful EIP-8130 delegation updates (not emitted in EVM).
    event DelegationApplied(address indexed account, address target);
    event AccountLocked(address indexed account, uint16 unlockDelay);
    event AccountUnlockInitiated(address indexed account, uint40 unlocksAt);

    // Account creation (factory)
    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 existing account (ERC-1271). chainId: 0 = multichain; else MUST equal block.chainid.
    function importAccount(address account, uint256 chainId, InitialActor[] calldata initialActors, bytes calldata signature) external;

    // Portable actor changes. chainId: 0 = multichain sequence; else local (MUST equal block.chainid).
    function applySignedActorChanges(address account, uint256 chainId, ActorChange[] calldata actorChanges, bytes calldata auth) external;

    // Account lock. Signed, relayable, admin-authorized. op: 1 = lock, 2 = unlock.
    // Local channel only: the contract binds the digest to block.chainid and the current local_sequence, which the op then increments.
    function applySignedLockChanges(
        address account,
        uint8 op,
        uint16 unlockDelay,
        bytes calldata auth
    ) external;

    // Signature verification and actor authentication
    // verifySignature: ERC-1271-style boolean check; returns false on any failure.
    // authenticateActor: returns the actor's authorization surface verbatim; reverts on failure.
    function verifySignature(address account, bytes32 hash, bytes calldata signature) external view returns (bool verified);
    function authenticateActor(address account, bytes32 hash, bytes calldata auth)
        external view returns (uint8 scope, address policyTarget);

    // Storage views
    function isActor(address account, bytes32 actorId) external view returns (bool);
    // Returns stored bytes unmodified (verbatim reporting).
    function getActorConfig(address account, bytes32 actorId) external view returns (ActorConfig memory);
    // Aggregate: (manager, commitment). Gating is determined by the POLICY scope bit; slots are zero when POLICY is unset.
    function getPolicy(address account, bytes32 actorId)
        external view returns (address target, bytes32 commitment);
    // Single-SLOAD accessors for the execution hot path.
    function getPolicyCommitment(address account, bytes32 actorId) external view returns (bytes32);
    function getPolicyManager(address account, bytes32 actorId) external view returns (address);
    function getChangeSequences(address account) external view returns (ChangeSequences memory);
    function isLocked(address account) external view returns (bool);
    // Decodes the lock_union / mode bit and folds in effective-unlock, so callers never see the packed layout.
    function getLockStatus(address account) external view returns (bool locked, bool hasInitiatedUnlock, uint40 unlocksAt, uint16 unlockDelay);
}
```

### IAuthenticator

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

### ITransactionContext (Precompile)

```solidity
interface ITransactionContext {
    function getTransactionSender() external view returns (address);
    function getTransactionPayer() external view returns (address);
    function getTransactionSenderActorId() external view returns (bytes32);
}
```

Read-only. Gas is charged as a base cost plus 3 gas per 32 bytes of returned data.

### INonceManager (Precompile)

```solidity
interface INonceManager {
    function getNonce(address account, uint256 nonceKey) external view returns (uint64);
}
```

Read-only. The protocol manages nonce storage directly; there are no state-modifying functions. Gas is charged as a base cost plus a cold SLOAD (2,100 gas) for the first read of a given `(account, nonceKey)` pair in the transaction, or a warm SLOAD (100 gas) for subsequent reads of the same slot, consistent with [EIP-2929](./eip-2929.md) access rules.

## Security Considerations

**Validation Surface**: For canonical authenticators, invalidators are `actor_config` changes and nonce consumption.

**Replay Protection**: Transactions include `chain_id`, 2D nonce (`nonce_key`, `nonce_sequence`), and `expiry`. For standard and 2D transactions (`nonce_key != NONCE_KEY_MAX`), replay protection and deduplication are provided by the nonce sequence: inclusion increments `(sender, nonce_key)`, so a given `(sender, nonce_key, nonce_sequence)` can be included at most once. `replay_id` does not apply to these transactions. For `NONCE_KEY_MAX` (nonce-free mode), there is no nonce slot, so replay protection relies on short-lived `expiry` and deduplication by the fee- and signature-invariant [Replay Identifier](#replay-identifier) (`replay_id`); the chain enforces a tight expiry window (`NONCE_FREE_EXPIRY_WINDOW`) to bound the window, and block builders MUST NOT include two transactions with the same `(sender, replay_id)` (see [Mempool Replacement](#mempool-replacement)).

The full transaction hash MUST NOT be used for nonce-free deduplication or mempool replacement. The transaction hash commits to fee fields (`max_fee_per_gas`, `max_priority_fee_per_gas`, `gas_limit`) and to `sender_auth` and `payer_auth` (the authorization blobs), all of which `replay_id` excludes. Keying on the transaction hash would allow trivial duplication of a single logical transaction, or would treat an intentional fee bump as an unrelated transaction:

- **Fee bumps**: a legitimate replacement raising `max_priority_fee_per_gas` (or `max_fee_per_gas`) changes the transaction hash while leaving the effects, `calls`, `metadata`, `expiry`, and `payer` identical. `replay_id` is unaffected, so the bump is recognized as a replacement of the same logical transaction rather than a distinct one; the bumped transaction still requires a fresh `payer_auth` when sponsored, since `payer_auth` commits to the fee fields.
- **Re-signed `payer_auth`**: the sender signs over the `payer` *address* but not the payer's signature bytes, so a sponsor can produce a different `payer_auth` for the same sender body, yielding a different transaction hash for the same logical transaction.
- **`sender_auth` non-determinism**: ECDSA signing is randomized (a fresh nonce `k` produces a different valid `(r, s)` for the same message and key) and signatures are additionally malleable, so the same key can produce multiple distinct-but-valid `sender_auth` values for one transaction body. Each recovers the same sender yet yields a different transaction hash.

All three cases resolve to the same `replay_id` (fee bumps included, since `replay_id` excludes the fee fields), so deduplicating and replacing on it collapses them to a single mempool slot and, ultimately, a single includable transaction. `expiry` cannot be extended via a fee-bump replacement: `expiry` is part of `replay_id`, so changing it produces a different `replay_id` and a new logical transaction, not a replacement of the old one which must independently satisfy nonce/sequence and mempool acceptance rules.

**Actor Scope and Policy**: Scope grants are protocol-enforced after authenticator execution during validation (fail closed). The policy gate is protocol-enforced during execution (`ActorPolicyViolation`). See [Actor Scope](#actor-scope) and [Actor Policies](#actor-policies).

**Policy Target as Trust Anchor**: For a policy-bearing actor, the resolved target is fully trusted to enforce the committed limits; the key can only reach that target, which decides what happens next. A buggy or malicious manager can do anything its own authority over the account allows. Accounts SHOULD point restricted keys only at audited managers and treat installing one with the same care as granting that contract authority over the account. A manager (and any policy it enforces) SHOULD be non-upgradeable: an upgradeable manager lets whoever controls the upgrade rewrite enforcement, which is equivalent to granting that party root access over everything the manager can do for the account. The committed parameters and any target-held state are the target's own authorization surface; checking that `keccak256(params)` matches the stored commitment is the target's responsibility, not the protocol's. How the target obtains authority to act for the account is out of scope for this specification.

**Policy State on Revocation**: `revokeActor` clears `actor_config`, `policy_commitment`, and `policy_manager` (all keyed by `(account, actorId)`), which immediately stops the key from reaching its target; there are no per-target protocol entries to enumerate or resurrect, and storage is fully reclaimed. Any parameters a target keeps in its own storage are not protocol state and are not auto-cleared; wallets uninstall them through the target when retiring a key, and an `expiry` bounds the window during which a not-yet-uninstalled key could otherwise be used.

**Actor Management**: Config change authorization requires the authorizing actor be admin (`scope == 0x00`). The EOA actor is implicitly authorized with unrestricted (admin) scope; revocable via portable config change. All actor modification paths are blocked when 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 (see [Actor Expiry](#actor-expiry)).

**Implicit EOA Rule Scoping**: The implicit EOA authorization rule only applies when authentication used the native secp256k1 path, either the EOA path (`sender` empty) or `K1_AUTHENTICATOR`, and the account's `DEFAULT_EOA_REVOKED` flag is unset. Generic authenticator contracts MUST NOT satisfy the implicit branch even if they return `bytes32(bytes20(sender))`, otherwise an arbitrary authenticator could authenticate as any EOA whose implicit actor slot has never been written.

**actorId Binding**: The protocol checks that the authenticator's returned `actorId` maps back to that authenticator in `actor_config`, preventing a malicious authenticator from claiming control of another authenticator's actors.

**Payer Security**: `AA_TX_TYPE` vs `AA_PAYER_TYPE` domain separation prevents signature reuse between sender and payer roles. The `payer` field in the sender's signed hash binds to a specific payer address. Scope enforcement adds a second layer: payer-scoped actors cannot be used as `sender_auth`, and vice versa; self-pay requires SELF_PAYER while sponsorship requires SPONSOR_PAYER, so a self-paying session key cannot be repurposed as an unbounded sponsor. The payer's exposure to sender-controlled gas is bounded by signed fee fields because `gas_limit` includes sender authentication, intrinsic costs, account changes, and call execution. Payer authentication uses the payer's chosen authenticator, is validated under SPONSOR_PAYER scope, and is metered separately so the payer's authenticator choice cannot reduce gas available to `calls` or otherwise affect execution behavior.

**Cross-sender Payer Replay**: The payer signature hash binds to the resolved sender via the `sender` field (see [Signature Payload](#signature-payload)). In the EOA path where `sender` is empty in the wire format, the recovered sender address MUST be substituted into the `sender` position before computing the hash. Without this substitution, two different EOAs that construct otherwise identical transaction data (same `chain_id`, `nonce_key`, `nonce_sequence`, `expiry`, fees, `account_changes`, `calls`) would produce identical payer hashes, allowing a second EOA to reuse a payer signature originally issued for the first and drain the payer's gas deposit. The 2D nonce alone does not prevent this: `nonce_key` and `nonce_sequence` are fields in the transaction payload, so each attacker controls their own values. Substituting the recovered sender into the hash makes the payer's commitment per-sender and closes this replay path. The configured-actor path is unaffected because `sender` is non-empty by definition.

**Account Creation Security**: `initial_actors` (`actorId`, `authenticator`, `scope`, and `policyData`; expiry is not expressible at create, per [Address Derivation](#address-derivation)) are salt-committed, preventing front-running of actor assignment. Wallet bytecode should be inert when uninitialized as it can be permissionlessly deployed. The create entry applies only to addresses that satisfy CREATE2 freshness. Without the nonce check, a create entry could be replayed against an EOA that has transaction history at the counterfactual address. Direct code placement also bypasses CREATE/CREATE2's [EIP-170](./eip-170.md) `MAX_CODE_SIZE` check, so the protocol enforces `len(code) <= MAX_CODE_SIZE` explicitly to keep the placed code within the EVM contract size limit.

## Copyright

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