EIP-8130 - Account Abstraction by Account Configuration

Created 2025-10-14
Status Draft
Category Core
Type Standards Track
Authors
Requires

Abstract

This proposal introduces a new EIP-2718 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 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:

Accounts are portable across EVM chains; see 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). Accounts MAY override with a delegation entry in account_changes or a standard EIP-7702 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). 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 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). 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)

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 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) is:

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

Constraints:

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.

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). 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). 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.

Field Bytes Description
authenticator 0–19 Authenticator contract address
scope 20 Permission bitmask (0x00 = unrestricted; also the admin predicate, see 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); 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:

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)
2 0x04 NONCE Permits a restricted actor to use sequenced nonce_keys for sender-context transactions (see 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).

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.

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). 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; see 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_keys 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.

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), 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?).

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), 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).

Scope. POLICY is the gated initiation grant. How it composes with SENDER is defined in 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); 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. 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.

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_ids 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.

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).

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:

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 below); the rationale for keying on replay_id instead is explained in Security Considerations.

Mempool Replacement

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

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), 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) validated as admin (scope == 0x00) against account. Anyone may relay; authorization comes from the signature.

Lifecyclelock, 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:

The signature is validated against the account via ERC-1271 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; 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 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), and accounts MAY set or clear delegation explicitly via a 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 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 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 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
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)
max_fee_per_gas Maximum fee per gas unit (EIP-1559)
gas_limit Maximum gas budget for sender-intrinsic gas (intrinsic gas excluding payer authentication) and call execution (see 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
calls Empty: No calls. Non-empty: Array of call phases; see Call Execution
metadata Empty: No metadata. Non-empty: Opaque attribution or annotation bytes. See Transaction Metadata
payer Gas payer identity. Empty: Sender pays. 20-byte address: This specific payer required. See Payer Modes
sender_auth See Signature Format
payer_auth Payer authorization. Empty: self-pay. Non-empty: authenticator \|\| data, same format as sender_auth. See 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-2028) 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), 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) 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. 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), 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 (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 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.

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 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). 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).

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 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) 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 actorIds 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) 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 actorIds:

// 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 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). 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: 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
  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.

Operation types:

change_type Name data Description
0x01 authorizeActor abi.encode(ActorConfig config, bytes policyData) (see ActorConfig) 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). 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).

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

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).

Delegation Entry

Delegation entries set EIP-7702-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), 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:

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). 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). 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?).

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?).

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):

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:

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 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). 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) and check scope per 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).
  6. Resolve payer from payer and payer_auth:
  7. 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.
  8. 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.
  9. 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.
  10. 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 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.
  11. Standard keys (nonce_key != NONCE_KEY_MAX): require nonce_sequence == current_sequence(sender, nonce_key).
  12. 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). Deduplicate by the Replay Identifier (replay_id), not the full transaction hash.
  13. Mempool threshold: gas payer's pending count below node-configured limits.
  14. Apply 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 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:

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).
  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)).
  6. Set transaction context on the Transaction Context precompile (sender, payer, actorId).
  7. Execute calls per 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 interface, following the protocol-injected log pattern established by EIP-7708. 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:

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), 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 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)
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)
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

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 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. 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), 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). 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)
  4. Front-running prevention: initial_actors in the salt prevents attackers from deploying with different actors (see Create Entry)

Why Delegation via Account Changes?

EIP-7702 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 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 is adopted, the protocol could be extended with a single canonical external factory. The create entry would emit an EIP-7702 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:

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:

Backwards Compatibility

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

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.

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

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

ITransactionContext (Precompile)

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)

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 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_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).

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:

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 and 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).

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). 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) 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 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.