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 a 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:
Actors and Authenticators — the authentication and authorization model(see Authenticators).
Account Configuration — onchain management of account and it’s actors (see Account Configuration).
AA Transaction Type — the wire format, signature payloads, and gas accounting (see AA Transaction Type).
Account Changes — how accounts are created and how actors are added, revoked, or delegated (see Account Changes).
Note that the accounts are portable to any EVM and the system is able to work on all chains. If a chain does not support 8130, an alternative AA mechanism such as ERC-4337 can be used with the same account, provided the account code allows support.
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 1559/7702 transaction flows
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 (policyType != 0x00) 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:
No nesting (depth-1): the nested authenticator MUST NOT be the delegate authenticator; delegation cannot chain.
Nested authenticator MUST be canonical: nodes apply the authenticator allowlist to the nested authenticator just as to the outer one, keeping total work bounded and enshrinable.
SIGNATURE scope on the nested actor: the nested check is B vouching via a signature, so it runs in B’s SIGNATURE context.
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, policy type, and an optional expiry. When policyType != 0x00, 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 native secp256k1 config is held inline in the packed account-state slot — the default-EOA scope/policyType/expiry fields plus the DEFAULT_EOA_REVOKED bit (see Account Lock) — so the account’s own key resolves in a single SLOAD, including the scoped-self case. Its actor_config slot is instead reserved for a non-secp256k1 self authenticator (e.g. a authenticator that returns the self-actorId); the inline secp256k1 self and a non-k1 self are mutually exclusive (registering one clears the other). Revoking the self-actor writes the account-state slot and leaves the actor_config slot empty and reusable.
Field
Bytes
Description
authenticator
0–19
Authenticator contract address
scope
20
Permission bitmask (0x00 = unrestricted; see Actor Scope)
expiry
21–26
uint48 Unix timestamp (seconds); the actor is invalid once block.timestamp > expiry. 0 = no expiry
policyType
27
Policy selector: 0x00 = no policy; any non-zero value gates the actor to its stored policy_manager. The specific non-zero value is interpreted by the manager, not the protocol (see Actor Policies)
reserved
28–31
Reserved for future use (must be zero)
When policyType != 0x00, the actor’s policy is held in two additional slots:
policy_commitment(account, actorId) → bytes32 // set when policyType != 0x00
policy_manager(account, actorId) → address // set when policyType != 0x00
These slots are read only during execution (see Actor Policies); validity is still decided by the single actor_config SLOAD.
Implicit EOA authorization: A native secp256k1 signature recovering to the account authenticates as the self-actor whenever the account’s DEFAULT_EOA_REVOKED flag is not set, resolved from the inline default-EOA config. For a fresh account that inline config is all-zero, scope and policyType are 0x00 and expiry is 0 (full permissions). This allows every existing EOA to send AA transactions immediately without prior registration. When this path applies, the protocol authenticates using native ecrecover rather than calling an external authenticator contract.
The default EOA is disabled by setting the DEFAULT_EOA_REVOKED flag in the packed account-state slot, which gates the inline secp256k1 self in its entirety. The self key can also be registered with K1_AUTHENTICATOR (address(1)) to set a custom scope, policyType, or expiry while retaining native ecrecover authentication; this writes those values into the inline default-EOA fields (a non-zero scope downgrades the key) and leaves the flag clear. Registering a non-secp256k1 self authenticator instead writes the actor_config(self) slot and sets the flag, disabling the inline k1 self (the two are mutually exclusive). createAccount and importAccount set the flag by default, so a newly created or imported account does not leave a native secp256k1 owner live unless one is among initial_actors which is a quantum-safe default.
Actor Scope
The scope byte in actor_config is a permission bitmask that restricts which authorization contexts an actor can be used in. A value of 0x00 means unrestricted and the actor is valid in all contexts. Any non-zero value restricts the actor to contexts where the corresponding bit is set.
Bit
Value
Name
Context
0
0x01
SIGNATURE
ERC-1271 via verifySignature()
1
0x02
SENDER
sender_auth validation, can originate transactions
2
0x04
PAYER
Gas payment — payer_auth validation (sponsored) and the sender’s own gas (self-pay)
3
0x08
CONFIG
Config change auth
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 — sender_auth (SENDER), gas payment by either the sender or a sponsor (PAYER), verifySignature() (SIGNATURE), or config change auth (CONFIG). The actor is authorized for that context when it is unrestricted (scope == 0x00) or the context’s bit is set ((scope & context_bit) != 0); otherwise the actor is not authorized and the request is rejected.
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 Policies
The policyType byte selects whether an actor is gated. 0x00 means no policy; any non-zero value gates the actor to a stored manager and carries an opaque 32-byte commitment (keccak256 of the policy parameters). The protocol gates identically on every non-zero value and never interprets the specific byte; it is carried in actor_config for the manager to read as a sub-type if useful.
policyType
Gate: call.to MUST equal
policyData
0x00
(no gate)
—
non-zero
the actor’s manager
manager (20 bytes) ‖ commitment (32 bytes)
A policy-bearing actor (typically a session key) may call exactly one target: its configured manager. The contract at that target reads the actor’s commitment (via getPolicy), validates the presented policy parameters against it (keccak256(params) == commitment), enforces what the call may do, and carries out the approved action. The protocol’s only responsibility is the single-target gate; how the target enforces the commitment and how it acts on behalf of the account are out of scope for this specification.
A key that should be enforced by the account’s own code rather than a separate contract sets manager = account.
Scope. The policy gate constrains only SENDER-context calls, so a policy-bearing actor MUST be authorized with a restricted scope that is neither unrestricted (0x00) nor includes CONFIG (0x08): a CONFIG-scoped key could authorize new, unrestricted actors and escape its policy entirely. PAYER and SIGNATURE scopes are permitted but are not policy-constrained — a policy-bearing actor with PAYER scope can sponsor gas up to the transaction’s limits, and with SIGNATURE scope can produce ERC-1271 signatures the policy never sees, so wallets SHOULD grant a restricted key only the scopes it needs, typically SENDER alone.
Reference flow (non-normative). A session-key policy with a custom manager:
The account authorizes the key with a non-zero policyType, a manager, and commitment = keccak256(params).
The account installs params at the manager, which checks that getPolicy(account, actorId) resolves to itself with a matching commitment.
On each use, the protocol gate routes the key’s call to the manager; the manager re-reads getPolicy(account, actorId), requires keccak256(presented params) == commitment, enforces them, and acts for the account.
revokeActor (or expiry) clears the slots, so step 3 then fails for that key.
The commitment is signed, opaque, and protocol-stored. Because it is part of the signed actor change, one signature fully describes the key’s authority (its manager and exact policy) and travels with the portable, multichain actor-change path. Because it is opaque (one word), the protocol stays agnostic to the policy vocabulary: new policy behaviors are new target logic, requiring no protocol change.
Enforcement is at execution, not validation. The protocol does not read policy_commitment or policy_manager when checking transaction validity; the gate is applied later, during Call Execution.
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. The nonce_key_cost for this mode (see Intrinsic Gas) covers replay_id deduplication state, which is distinct from the nonce counter and is maintained to detect duplicate transactions within the expiry window.
Nodes SHOULD reject NONCE_KEY_MAX transactions from the mempool if expiry exceeds a short window (e.g., 10 seconds from current time). Replay protection is handled by the nonce-free replay identifier defined below.
Nonce-Free Replay Identifier
Nonce-free deduplication and replay protection MUST key on a signature-invariant identifier rather than the EIP-2718 transaction hash:
where sender_signature_hash is defined in Signature Payload and 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.
The full transaction hash MUST NOT be used for nonce-free deduplication; the rationale for keying on replay_id instead is explained in Security Considerations.
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
locked
Actor configuration is frozen — config changes rejected
unlock_delay
Seconds required between initiating unlock and becoming unlocked (uint16)
unlocks_at
Timestamp when unlock takes effect (uint40, 0 = no unlock initiated)
flags
Account flags byte; bit 0 (DEFAULT_EOA_REVOKED) disables the secp256k1 self-actor
default_eoa_scope
Inline self-actor scope (uint8; 0x00 = full owner)
default_eoa_policy_type
Inline self-actor policyType (uint8; 0x00 = none)
default_eoa_expiry
Inline self-actor expiry (uint48 Unix seconds; 0 = no expiry)
The packed slot is exactly 32 bytes, so the inline self-actor config costs no extra SLOAD/SSTORE beyond the account-state access already performed for the change-sequence and lock fields.
When locked is set, all config changes are rejected — both config change entries in account_changes and applySignedActorChanges() via EVM. The lock cannot be removed without a timelock delay.
Lock operations are called directly by the account (msg.sender) on the Account Configuration Contract.
Lifecycle:
Lock: Call lock(unlockDelay). Sets locked = true with the specified unlockDelay (seconds).
Effective unlock: Once block.timestamp >= unlocks_at, the account is effectively unlocked — config changes are permitted.
Account Import
importAccount(address account, 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. The call is rejected when:
The account already has 8130 state — either change-sequence channel is non-zero. Import is a one-time bootstrap, so it requires both the local and multichain channels empty.
The account is locked. Locking is keyed on msg.sender and does not require initialization, so a locked account is not necessarily initialized; this check is independent (see Account Lock).
The signature is validated against the account via ERC-1271isValidSignature(digest, signature), binding the initial actor set to the account’s existing authorization logic. digest is a typed ActorInitialization struct hash:
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. This typed style is distinct from the packed style used for Address Derivation; that split is intentional.
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.
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
Unix timestamp (seconds since epoch). Transaction invalid when block.timestamp > expiry. A value of 0 means no expiry. Must be non-zero when nonce_key == NONCE_KEY_MAX.
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 bytes carrying attribution or annotation data. The protocol does not interpret or execute it. See Transaction Metadata
payer
Gas payer identity. Empty: Sender pays. 20-byte address: This specific payer required. See Payer Modes
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.
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:
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), so the separation governs execution isolation, not who bears the cost.
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.
payer_auth_cost: 0 for self-pay (payer empty). Otherwise, the same sender_auth_cost model applies to the payer’s authenticator.
Component
Value
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: 14,000 gas (replay protection state: 2 cold SLOADs + 1 warm SLOAD + 3 warm SSTORE resets). 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; initial actors are unrestricted owners, so no policy_commitment/policy_manager slots). 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 policyType != 0x00). 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/policyType/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
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.
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).
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, policyType, and expiry from the inline fields (all-zero = unrestricted, non-expiring full owner). Otherwise SLOAD actor_config(sender, actorId) 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.
Check scope: Read the resolved scope byte (from the inline default-EOA config for the secp256k1 self-actor, or actor_config otherwise). Determine the context bit: 0x02 (SENDER) for sender_auth, 0x04 (PAYER) for payer_auth, 0x01 (SIGNATURE) for verifySignature(), 0x08 (CONFIG) for config change auth. Require scope == 0x00 || (scope & context_bit) != 0. In self-pay (no payer_auth), the sender’s own actor is the gas payer, so additionally require PAYER scope on the resolved sender actor (scope == 0x00 || (scope & 0x04) != 0).
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:
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 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 PAYER scope
address
authenticator (20) \|\| data
payer field
Sponsored — any authenticator. Reads payer’s actor_config, validates against payer address, and requires 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 does not affect validation or execution: the protocol never parses, dispatches, or otherwise interprets it. It 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. Off-chain consumers (indexers, explorers) read it directly from the transaction.
The byte layout of metadata is not constrained by this specification; producers MAY use any encoding, and MAY treat the field as wholly opaque bytes. A companion specification defines a recommended structured encoding so that off-chain consumers can index metadata interoperably.
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 account)
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 with CONFIG scope (an unrestricted 0x00 scope also satisfies this). 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:
Initial actors are registered as unrestricted owners (scope = 0x00, policyType = 0x00, no expiry). Only authenticator and actorId are required; scope, expiry, and policy are not expressible in the create entry and default to their zero values. Additional actors with restricted scope or policies are configured after deployment via config change entries.
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:
The per-actor contribution is actorId || authenticator (52 bytes: the 32-byte actorId and the 20-byte authenticator address). Because initial actors are always unrestricted owner keys, no scope, expiry, or policy fields participate in the commitment. 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:
Parse [0x00, user_salt, code, initial_actors] where each entry is [actorId, authenticator]
Require initial_actors are sorted by actorId in strictly ascending order; reject any unsorted set (strict ascending order also rejects duplicate actorId values).
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
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)
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, // uint64: 0 = valid on any chain
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, expiry (0 = no expiry), and policyType. Slices policyData into policy_commitment/policy_manager: empty for 0x00, manager ‖ commitment when non-zero (see Actor Policies); rejects mismatched len(policyData), and rejects a policy-bearing actor whose scope is unrestricted (0x00) or includes CONFIG (0x08). Emits ActorAuthorized.
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 have CONFIG scope, which is satisfied by an unrestricted actor (scope == 0x00) or by one whose scope sets the CONFIG bit (0x08); see Actor Scope. Throughout this specification, a requirement for “CONFIG scope” (or any other context scope) is met by either the unrestricted 0x00 scope or the corresponding context bit.
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:
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:
account_changes (tx field) — processed by the protocol before code deployment on 8130 chains.
applySignedActorChanges() (EVM) — applied during EVM execution on any chain, including non-8130 chains and ERC-4337 deployments.
Both paths carry the same signed actor changes, share the same change_sequence counters, and are equally portable (chain_id 0 for cross-chain or a specific chain_id for chain-local). They differ only in transport: the protocol consumes the signed change directly on 8130 chains, while everywhere else the EVM function does. applySignedActorChanges() parses the authenticator address from auth, calls the authenticator to get the actorId, and checks actor_config. authorizeActor writes actor_config and, for policy-bearing actors (policyType != 0x00), 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. All actor modification paths are blocked when the account is locked (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 be the account’s implicit EOA actor (actorId == bytes32(bytes20(sender))) with CONFIG scope (an unrestricted 0x00 scope also satisfies this).
Delegation Format
rlp([
0x02, // type: delegation
target // address: delegate to this contract, or address(0) to clear
])
The delegation is only permitted when:
code_size(sender) == 0 (empty account), or
code(sender) starts with the delegation designator 0xef0100 (updating an existing delegation)
It will not replace non-delegation bytecode.
When target is address(0), the delegation indicator is cleared and the account’s code hash is reset to the empty code hash.
For 8130 transactions, successful delegation updates emit a protocol-injected DelegationChanged(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:
Create entry (if present): Register initial_actors in Account Config storage for sender — for each [actorId, authenticator] tuple, write actor_config as an unrestricted owner (the authenticator address with scope = 0x00, expiry = 0, policyType = 0x00; no policy slots). Mark the account initialized by setting its local change-sequence channel to 1 (see Config Change Authorization). Initialize lock state to safe defaults: locked = false, unlock_delay = 0, unlocks_at = 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.
Config change entries (if any): Apply operations in entry order. Reject transaction if account is locked.
Delegation entries (if any): Require the sender’s resolved actorId == bytes32(bytes20(sender)) (EOA actor only) with CONFIG scope (unrestricted 0x00 also satisfies this). 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 — 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 policyType != 0x00, 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):
The frame’s return data is abi.encodeWithSelector(ActorPolicyViolation.selector, actorId, call.to). This is a consensus-level result, not a validity error, so standard atomicity applies: the enclosing phase’s state changes roll back, later phases are skipped, and the phase is reported as failed in phaseStatuses. Only work already performed is charged (intrinsic gas plus the one policy_manager SLOAD); the undispatched call body costs nothing and the transaction is still included with its nonce consumed.
Common patterns:
Simple call: [[{to, data}]] — one phase, one call
Atomic batch: [[call_a, call_b, call_c]] — one phase, all-or-nothing
Sponsor + user: [[sponsor_payment], [user_action_a, user_action_b]] — sponsor in phase 0 (committed), user actions in phase 1 (atomic, skipped if sponsor fails)
Transaction Context
The Transaction Context precompile at TX_CONTEXT_ADDRESS provides read-only access to the current AA transaction’s metadata during call execution. The precompile is populated only while the protocol is dispatching the transaction’s calls; validation and account-change processing do not populate it. It reads directly from the client’s in-memory transaction state — protocol “writes” are effectively zero-cost. Gas is charged as a base cost plus 3 gas per 32 bytes of returned data, matching CALLDATACOPY pricing.
Function
Returns
Available
getTransactionSender()
address — the account executing calls (sender)
Execution only
getTransactionPayer()
address — gas payer (sender for self-pay, payer for sponsored)
Execution only
getTransactionSenderActorId()
bytes32 — authenticated actor’s actorId
Execution only
If the wallet needs the authenticator address or scope, it calls getActorConfig(account, actorId) on the Account Configuration Contract. A policy target reached as a call.to identifies which key it is acting for by combining getTransactionSender() and the authenticated getTransactionSenderActorId() from this precompile, then reads the actor’s policy sub-type, 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)
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
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).
Resolve sender: if sender set, use it; if empty, ecrecover from sender_auth
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
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.
Validate sender_auth against resulting actor state (see Validation). Require SENDER scope on the resolved actor. If the transaction is self-pay, also require PAYER scope on the resolved sender actor. If delegation entries are present, also require actorId == bytes32(bytes20(sender)) (EOA actor) and CONFIG scope. Each scope requirement is satisfied by an unrestricted (0x00) actor or by the corresponding context bit.
Resolve payer from payer and payer_auth:
payer empty and payer_auth empty: self-pay. Payer is sender; the resolved sender actor’s PAYER scope authorizes payment. Reject if balance insufficient.
payer = 20-byte address (sponsored): payer_auth uses any authenticator. Validate payer_auth against the payer address’s actor_config. Require PAYER scope on the resolved actor.
Verify nonce, payer ETH balance, and expiry:
Standard keys (nonce_key != NONCE_KEY_MAX): require nonce_sequence == current_sequence(sender, nonce_key).
Nonce-free key (nonce_key == NONCE_KEY_MAX): skip nonce check, require nonce_sequence == 0, require non-zero expiry, and nodes SHOULD reject if expiry exceeds a short window (e.g., 10 seconds). Deduplicate by the Nonce-Free Replay Identifier (replay_id), not the full transaction hash.
Mempool threshold: gas payer’s pending count below node-configured limits.
Nodes MAY apply higher pending transaction rate limits based on account lock state:
Locked sender: A locked sender account has a stable signature if combined with a stateless authenticator. Nodes can safely allow a higher sender rate.
Locked payer with trusted bytecode: A locked payer account whose bytecode is recognized (e.g. a canonical account implementation that restricts ETH movement while locked) provides an additional guarantee that ETH balance only decreases via gas fees. Nodes can safely allow a higher payer rate for such accounts, supporting high-throughput payer/sponsor services.
Block Execution
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 the sender’s resolved actorId == bytes32(bytes20(sender)) (EOA actor) with CONFIG scope (unrestricted 0x00 also satisfies this).
ETH gas deduction from payer (sender for self-pay). Transaction is invalid if payer has insufficient balance.
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).
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.
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, DelegationChanged) 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:
payer (address): Gas payer address (sender for self-pay, specified payer for sponsored).
status (uint8): 0x01 = all phases succeeded (or calls was empty), 0x00 = one or more phases reverted. Existing tools checking status == 1 remain correct for the success path.
phaseStatuses (uint8[]): Per-phase status array. Each entry is 0x01 (success) or 0x00 (reverted). Phases after a revert are not executed and reported as 0x00. Empty if calls was empty.
Account-state flags bit that disables the implicit default-EOA path
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)
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 No Public Key Storage?
Public keys are not stored in the Account Configuration Contract. Instead, actors are identified by actorId (bytes32) and public key material is provided at signing time in the authenticator-specific data portion of the signature. This design is motivated by three factors:
State growth: Public key storage is permanent state growth. For PQ keys (1,000+ bytes), this means 40+ storage slots per actor. Calldata goes to data availability (temporary); storage is permanent. The trend is toward higher SLOAD costs and cheaper DA.
Gas efficiency: Calldata is cheaper than cold SLOADs for all key sizes. P256: ~2,048 gas calldata vs ~6,300 gas cold SLOADs. PQ: ~21,000 gas calldata vs ~88,000 gas cold SLOADs. Transaction validity requires only one cold SLOAD on actor_config; policy slots (policy_commitment, policy_manager) are read only during execution, so policy-free actors pay nothing at validation time.
Simplicity: One primary storage slot per actor (actor_config). No variable-length public key encoding, no multi-slot public key reads, no length fields. Registration is a single SSTORE in the common case, with two extra SSTOREs (policy_commitment and policy_manager) when an actor carries a policy.
The protocol never needs to know how any algorithm works.
Why Policy Types?
Session keys and gas-payer hot wallets often need narrow authority: only this token, only this much per day, only this action. policyType expresses that by gating a restricted key to a single call target and binding it to a signed, opaque commitment that the target enforces. The protocol’s only policy responsibility is the single-target gate plus storing the commitment; what the target does with the call, and how it ultimately effects actions for the account, is left to the application.
Because the Account Configuration Contract is a single immutable deployment, it reserves no specific policyType values and gates on any non-zero value, leaving the byte for a manager to interpret. New policy behaviors are new manager logic, not new protocol values. Encoding policy primitives (selector lists, allowlists, spend limits) into consensus would freeze the vocabulary and require a fork per new primitive — an opaque commitment keeps the protocol agnostic and new behaviors shippable permissionlessly.
Why Actor Expiry?
Session keys are intended to be short-lived, but revocation requires a transaction, and a forgotten key lingers indefinitely. The per-actor expiry lets a wallet provision a key that lapses automatically at a chosen time, with no revoke transaction and no lingering authority. Because expiry is packed into the same actor_config slot the protocol already reads for validity, enforcement costs nothing extra and happens at validation: an expired actor’s transactions are rejected before execution rather than reverting. For policy-bearing keys it also caps how long the key can reach its target: once expired, the key can no longer act even if the policy parameters held by the target are never explicitly uninstalled. A value of 0 preserves the existing always-valid behavior for actors that should not expire.
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 signature-invariant Nonce-Free Replay Identifier. This is useful for operations where nonce ordering coordination is undesirable.
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:
Deterministic addresses: Same user_salt + code + initial_actors produces the same address on any chain
Pre-deployment funding: Users can receive funds at counterfactual addresses before account creation
Portability: Same deployment_code produces the same address on both 8130 and non-8130 chains (see Address Derivation)
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 the account’s implicit EOA actor (actorId == bytes32(bytes20(sender))) so that code delegation remains portable across non-8130 chains via standard EIP-7702 transactions. Eventually this can be expanded to all authenticator types, not just K1 EOAs.
Why Call Phases?
Phases provide two atomic batching levels without per-call mode flags:
Atomic batching: One phase, all-or-nothing.
Sponsor protection: Payment in phase 0 persists even if user actions in phase 1 revert.
Why a Metadata Field?
Attribution and annotation data (builder/app codes, payment references, off-chain commitments) traditionally ride as a trailing “data suffix” on tx.input. The structured calls array has no such trailing location, so a dedicated optional metadata field gives this data a first-class, signed home without overloading a call. Keeping it opaque and out of execution means the protocol stays agnostic to its contents: annotation formats are defined by companion specifications, not by protocol changes. Binding it into both signature payloads makes it tamper-evident, unlike a mutable trailing suffix.
Why No Value in Calls?
The protocol dispatches each call directly to the specified to address with msg.sender = sender. Since every account has wallet bytecode (via auto-delegation or explicit deployment), ETH transfers route through wallet code via the CALL opcode — no capability is lost. Removing protocol-level value from calls means the protocol never moves ETH on behalf of the sender.
Why a Transaction Context Precompile?
Transaction context (sender, payer, calls, gas) is immutable transaction metadata — it never changes during execution. actorId is set after validation and available during execution only. A precompile is the natural fit:
Zero protocol write cost: The precompile reads directly from the client’s in-memory transaction struct — no HashMap insert, no journaling, no rollback tracking.
Pull model: Consumers read only what they need — policies read actorId to enforce per-actor limits, and accounts may inspect payer for fee reimbursement or access logic.
Forward compatible: New context fields are added as new precompile functions — no interface changes to IAuthenticator or existing authenticator contracts.
Backwards Compatibility
No breaking changes. Existing EOAs and smart contracts function unchanged. Adoption is opt-in:
EOAs continue sending standard transactions
ERC-4337 infrastructure continues operating
Accounts gain AA capabilities by configuring actors. EOAs sending their first AA transaction are auto-delegated to DEFAULT_ACCOUNT_ADDRESS if they have no code. EOAs MAY override with a delegation entry in account_changes (EOA-only authorization), a standard EIP-7702 transaction, or use a create entry in account_changes for custom wallet implementations
Reference Implementation
IAccountConfiguration
interfaceIAccountConfiguration{structChangeSequences{uint64multichain;// chain_id 0
uint64local;// chain_id == block.chainid; starts at 1 once initialized (created/imported), 0 = uninitialized
}structActorConfig{addressauthenticator;uint8scope;// 0x00 = unrestricted
uint48expiry;// Unix seconds; 0 = no expiry. Actor invalid once block.timestamp > expiry
uint8policyType;// 0x00 = none; any non-zero = gated to stored manager (value interpreted by the manager)
}// Minimal actor used for account creation and import (always unrestricted: scope=0x00, policyType=0x00, no expiry)
structInitialActor{bytes32actorId;addressauthenticator;}structActor{bytes32actorId;ActorConfigconfig;bytespolicyData;// sliced by policyType: empty (0x00); manager[20] || commitment[32] (non-zero)
}structActorChange{uint8changeType;// 0x01 = authorizeActor, 0x02 = revokeActor
bytes32actorId;bytesdata;// operation-specific (see Config Change Format)
}eventActorAuthorized(addressindexedaccount,bytes32indexedactorId,ActorConfigconfig,addresspolicyManager,bytes32policyCommitment);eventActorRevoked(addressindexedaccount,bytes32indexedactorId);eventAccountCreated(addressindexedaccount,bytes32userSalt,bytes32codeHash);eventAccountImported(addressindexedaccount);eventDelegationChanged(addressindexedaccount,addresstarget);eventAccountLocked(addressindexedaccount,uint16unlockDelay);eventAccountUnlockInitiated(addressindexedaccount,uint40unlocksAt);// Account creation (factory)
functioncreateAccount(bytes32userSalt,bytescalldatabytecode,InitialActor[]calldatainitialActors)externalreturns(address);functioncomputeAddress(bytes32userSalt,bytescalldatabytecode,InitialActor[]calldatainitialActors)externalviewreturns(address);// Import existing account (ERC-1271 verification for initial actor registration)
functionimportAccount(addressaccount,InitialActor[]calldatainitialActors,bytescalldatasignature)external;// Portable actor changes (direct authentication via authenticator + actor_config)
functionapplySignedActorChanges(addressaccount,uint64chainId,ActorChange[]calldataactorChanges,bytescalldataauth)external;// Account lock (called by the account directly)
functionlock(uint16unlockDelay)external;functioninitiateUnlock()external;// Signature verification and actor authentication
// verifySignature: ERC-1271-style boolean check; returns false on any failure.
// authenticateActor: resolves the authenticating actor and returns its full authorization surface; reverts on failure.
functionverifySignature(addressaccount,bytes32hash,bytescalldatasignature)externalviewreturns(boolverified);functionauthenticateActor(addressaccount,bytes32hash,bytescalldataauth)externalviewreturns(uint8scope,uint8policyType,addresspolicyTarget);// Storage views
functionisActor(addressaccount,bytes32actorId)externalviewreturns(bool);functiongetActorConfig(addressaccount,bytes32actorId)externalviewreturns(ActorConfigmemory);// Resolves the policy sub-type, gate target, and signed commitment:
// 0x00 -> (0x00, address(0), bytes32(0)); non-zero -> (policyType, manager, commitment)
functiongetPolicy(addressaccount,bytes32actorId)externalviewreturns(uint8policyType,addresstarget,bytes32commitment);functiongetChangeSequences(addressaccount)externalviewreturns(ChangeSequencesmemory);functionisLocked(addressaccount)externalviewreturns(bool);functiongetLockStatus(addressaccount)externalviewreturns(boollocked,boolhasInitiatedUnlock,uint40unlocksAt,uint16unlockDelay);}
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 NONCE_KEY_MAX (nonce-free mode), replay protection relies on short-lived expiry and deduplication by the signature-invariant Nonce-Free Replay Identifier (replay_id). The mempool enforces a tight expiry window (e.g., 10-30 seconds) to bound the window. Block builders MUST NOT include two NONCE_KEY_MAX transactions with the same replay_id.
The full transaction hash MUST NOT be used for nonce-free deduplication. The transaction hash commits to sender_auth and payer_auth (the authorization blobs), whereas replay_id excludes both. Keying on the transaction hash would allow trivial duplication of a single logical transaction:
Re-signed payer_auth: the sender signs over the payeraddress but not the payer’s signature bytes, so a sponsor can produce a different payer_auth for the same sender body, yielding a different transaction hash for the same logical transaction.
sender_auth non-determinism: ECDSA signing is randomized (a fresh nonce k produces a different valid (r, s) for the same message and key) and signatures are additionally malleable, so the same key can produce multiple distinct-but-valid sender_auth values for one transaction body. Each recovers the same sender yet yields a different transaction hash.
Both variants resolve to the same replay_id, so deduplicating on it collapses them to a single mempool slot and a single includable transaction.
Actor Scope and Policy: Scope is protocol-enforced after authenticator execution during validation. The policy gate is protocol-enforced during execution (a call.to other than the resolved target reverts with ActorPolicyViolation). Because it is an execution-time gate rather than a validity input, it is not a mempool filter for compromised keys (see Why Policy Types?). The gate covers only SENDER-context calls, so a policy-bearing actor cannot hold unrestricted (0x00) or CONFIG scope — a CONFIG-scoped key could authorize new actors and escape its policy — while any PAYER or SIGNATURE scope it holds is not policy-constrained (see 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 CONFIG scope (satisfied by an unrestricted 0x00 scope or the CONFIG bit 0x08). The EOA actor is implicitly authorized with unrestricted scope; revocable via portable config change. All actor modification paths are blocked when the account is locked.
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-only actors cannot be used as sender_auth, and vice versa. 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 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 (the actorId and authenticator; scope, expiry, and policy fields are zero for the unrestricted owner keys created here, 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-170MAX_CODE_SIZE check, so the protocol enforces len(code) <= MAX_CODE_SIZE explicitly to keep the placed code within the EVM contract size limit.