--- eip: 8130 title: Account Abstraction by Account Configuration description: Enable account abstraction feature set through onchain account configurations and a new transaction type author: Chris Hunter (@chunter-cb) discussions-to: https://ethereum-magicians.org/t/eip-8130-account-abstraction-by-account-configurations/25952 status: Draft type: Standards Track category: Core created: 2025-10-14 requires: 2718 --- ## Abstract This proposal introduces a new [EIP-2718](./eip-2718.md) transaction type and an onchain Account Configuration system that together provide account abstraction — custom authentication, call batching, and gas sponsorship. Accounts register owners with onchain verifier contracts. Transactions declare which verifier to use, enabling nodes to filter transactions without executing 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 verification from account logic. Each transaction explicitly declares its verifier — a contract that takes a hash and signature data and returns the authenticated owner. This makes validation predictable: wallets know the rules, and nodes can see exactly what computation a transaction requires before executing it. Nodes may optionally filter on verifier identity, accepting only known verifiers (ECDSA, P256, WebAuthn, multisig, post-quantum) and rejecting the rest without execution. New signature algorithms are introduced through verifier contracts and standardized through the canonical verifier set. ## Specification ### Constants | Name | Value | Comment | |------|-------|---------| | `AA_TX_TYPE` | TBD | [EIP-2718](./eip-2718.md) transaction type | | `AA_PAYER_TYPE` | TBD | Magic byte for payer signature domain separation | | `AA_BASE_COST` | 15000 | Base intrinsic gas cost | | `ACCOUNT_CONFIG_ADDRESS` | TBD | Account Configuration system contract address | | `ECRECOVER_VERIFIER` | `address(1)` | Native secp256k1 (ECDSA) verifier for explicit k1 key registration | | `REVOKED_VERIFIER` | `type(uint160).max` | Revocation marker written to implicit EOA owner slot to block re-authorization | | `NONCE_MANAGER_ADDRESS` | TBD | Nonce Manager precompile address | | `TX_CONTEXT_ADDRESS` | TBD | Transaction Context precompile address | | `DEFAULT_ACCOUNT_ADDRESS` | TBD | Default wallet implementation for auto-delegation | | `DEPLOYMENT_HEADER_SIZE` | 14 | Size of the deployment header in bytes | | `NONCE_KEY_MAX` | `2^256 - 1` | Nonce-free mode (expiry-only replay protection) | ### Account Configuration Each account can authorize a set of owners through the Account Configuration Contract at `ACCOUNT_CONFIG_ADDRESS`. This contract handles owner authorization, account creation, change sequencing, and delegates signature verification to onchain [Verifiers](#verifiers). Owners are identified by their `ownerId`, a 32-byte identifier derived by the verifier from public key material. The protocol does not enforce a derivation algorithm — each verifier defines its own convention (see [ownerId Conventions](#ownerid-conventions)). Owners can be modified via calls within EVM execution by calling the authenticated config change functions. **Default behavior**: The EOA owner is implicitly authorized by default but can be revoked on the contract. #### Storage Layout Each owner occupies a single `owner_config` slot containing the verifier address (20 bytes) and a scope byte (1 byte) with 11 bytes reserved. The scope byte controls which authentication contexts the owner is valid for (see [Owner Scope](#owner-scope)). Non-EOA owners are revoked by deleting the `owner_config` slot. The implicit EOA owner (`ownerId == bytes32(bytes20(account))`) is revoked by overwriting the slot with `verifier = REVOKED_VERIFIER` (`type(uint160).max`), making it distinguishable from an empty (implicitly authorized) slot. | Field | Bytes | Description | |-------|-------|-------------| | `verifier` | 0–19 | Verifier contract address | | `scope` | 20 | Permission bitmask (`0x00` = unrestricted) | | reserved | 21–31 | Reserved for future use (must be zero) | **Implicit EOA authorization**: An unregistered owner (`owner_config` slot is empty) is implicitly authorized if `ownerId == bytes32(bytes20(account))`. The empty slot's scope byte is `0x00` (unrestricted), granting full permissions by default. This allows every existing EOA to send AA transactions immediately without prior registration. When the implicit rule applies, the protocol verifies using native ecrecover rather than calling an external verifier contract. The implicit authorization is revoked by writing `REVOKED_VERIFIER` (`type(uint160).max`) to the verifier field, making the slot non-empty and blocking re-authorization. The EOA owner can also be explicitly registered with `ECRECOVER_VERIFIER` (`address(1)`) to set a custom scope while retaining native ecrecover verification. #### Owner Scope The scope byte in `owner_config` is a permission bitmask that restricts which authentication contexts an owner can be used in. A value of `0x00` means unrestricted — the owner is valid in all contexts. Any non-zero value restricts the owner 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 | | 2 | `0x04` | PAYER | `payer_auth` validation | | 3 | `0x08` | CONFIG | Config change `auth` | The protocol checks scope after verifier execution: `scope == 0x00 || (scope & context_bit) != 0`. The protocol validates signatures by reading `owner_config` directly and delegating authentication to [Verifiers](#verifiers) — see [Validation](#validation) for the full flow. Owner enumeration is performed off-chain via `OwnerAuthorized` / `OwnerRevoked` event logs. No owner count is enforced on-chain — gas costs naturally bound owner creation. #### 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 nonce state. `nonce_sequence` MUST be `0`. Replay protection relies on `expiry`, which MUST be non-zero. 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 transaction hash. #### Account Lock Account lock state is stored in a single packed 32-byte slot: | Field | Description | |-------|-------------| | `locked` | Owner 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) | When `locked` is set, all config changes are rejected — both config change entries in `account_changes` and `applySignedOwnerChanges()` 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**: 1. **Lock**: Call `lock(unlockDelay)`. Sets `locked = true` with the specified `unlockDelay` (seconds). 2. **Initiate unlock**: Call `initiateUnlock()`. Sets `unlocks_at = block.timestamp + unlock_delay`. 3. **Effective unlock**: Once `block.timestamp >= unlocks_at`, the account is effectively unlocked — config changes are permitted. ### Delegation Indicator This proposal uses the same delegation indicator behavior as [EIP-7702](./eip-7702.md) on 8130 chains, even if [EIP-7702](./eip-7702.md) transactions are not enabled. An account is delegated when its code is exactly `0xef0100 || target`, where `target` is a 20-byte address. Delegated accounts MAY originate transactions, and all code-executing operations targeting a delegated account MUST load code from `target` instead of the indicator. ### Verifiers Each owner is associated with a verifier, a contract that performs signature verification. The verifier address is stored in `owner_config`. All verifiers implement `IVerifier.verify(hash, data)`. Stateful verifiers MAY read transaction context (sender address, calls, payer) from the Transaction Context precompile at `TX_CONTEXT_ADDRESS` (see [Transaction Context](#transaction-context)). The protocol validates the returned `ownerId` against `owner_config` and checks the owner's scope against the authentication context. Verifiers are executed via STATICCALL. Verifier addresses MUST NOT be delegated accounts — reject if the code at the verifier address starts with the delegation indicator (`0xef0100`). Execution is metered (see [Mempool Acceptance](#mempool-acceptance) for rules). On the 8130 path, nodes MAY enshrine canonical verifier execution and charge standard gas targets for those verifiers. Enshrined execution MUST produce identical results to the corresponding verifier contract. `ECRECOVER_VERIFIER` (`address(1)`) is a protocol-reserved address for native secp256k1 verification. When the protocol encounters this address as a verifier 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 `ownerId` is `bytes32(bytes20(recovered_address))`. Owners can be explicitly registered with `ECRECOVER_VERIFIER` to use native ecrecover with a custom scope, without requiring a deployed verifier contract. Any contract implementing `IVerifier` can be permissionlessly deployed and registered as an owner's verifier. #### Canonical Verifier Set This specification defines a canonical verifier set which is the set of signature algorithms that compliant nodes MUST accept. The initial canonical set includes: | Name | Algorithm | Verifier | |------|-----------|----------| | k1 | secp256k1 | `ECRECOVER_VERIFIER` (native sentinel) | | p256 | P-256 | Onchain contract | | passkey | WebAuthn / FIDO2 | Onchain contract | | delegate | Signature delegation | Onchain contract | The canonical verifier set and corresponding contract addresses are maintained in a companion ERC 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 verifiers in their allowlist and SHOULD NOT extend the allowlist with non-canonical verifiers. The 8130 path is intended to use a small, standard set of signature algorithms; accepting additional verifiers is a divergence from this specification. ### Account Types This proposal supports three paths for accounts to use AA transactions: | Account Type | How It Works | Key Recovery | |--------------|--------------|--------------| | **Existing Smart Contracts** | Already-deployed accounts (e.g., ERC-4337 wallets) register owners via the system contract (see [Smart Wallet Migration Path](#smart-wallet-migration-path)) | Wallet-defined | | **EOAs** | EOAs send AA transactions using their existing secp256k1 key via native ecrecover. If the account has no code, the protocol auto-delegates to `DEFAULT_ACCOUNT_ADDRESS` (see [Block Execution](#block-execution)). Accounts MAY override with a delegation entry in `account_changes` or a standard [EIP-7702](./eip-7702.md) transaction | Wallet-defined; EOA recoverable via 1559/7702 transaction flows | | **New Accounts (No EOA)** | Created via a create entry in `account_changes` with CREATE2 address derivation; runtime bytecode placed at address, owners + verifiers configured, `calls` handles initialization | Wallet-defined | ### AA Transaction Type A new [EIP-2718](./eip-2718.md) transaction with type `AA_TX_TYPE`: ``` AA_TX_TYPE || rlp([ chain_id, sender, // Sender address (20 bytes) | empty for EOA signature nonce_key, // uint256: nonce channel selector nonce_sequence, // uint64: sequence number expiry, // Unix timestamp (seconds) max_priority_fee_per_gas, max_fee_per_gas, gas_limit, account_changes, // Account creation, config change, and/or delegation operations | empty calls, // [[{to, data}, ...], ...] | empty payer, // empty = sender-paid, payer_address = specific payer sender_auth, payer_auth // empty = sender-pay, verifier || data = sponsored (same format as sender_auth) ]) call = rlp([to, data]) // to: address, data: bytes ``` #### Field Definitions | Field | Description | |-------|-------------| | `chain_id` | Chain ID per [EIP-155](./eip-155.md) | | `sender` | Sending account address. **Required** (non-empty) for configured owner signatures. **Empty** for EOA signatures—address recovered via ecrecover. The presence or absence of `sender` is the sole distinguisher between EOA and configured owner 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`. | | `max_priority_fee_per_gas` | Priority fee per gas unit ([EIP-1559](./eip-1559.md)) | | `max_fee_per_gas` | Maximum fee per gas unit ([EIP-1559](./eip-1559.md)) | | `gas_limit` | Maximum gas budget for intrinsic costs, sender authentication, account changes, and call execution (see [Intrinsic Gas](#intrinsic-gas)). Payer authentication is metered separately. | | `account_changes` | **Empty**: No account changes. **Non-empty**: Array of typed entries — create (type `0x00`) for account deployment, config change (type `0x01`) for owner management, and delegation (type `0x02`) for code delegation. See [Account Changes](#account-changes) | | `calls` | **Empty**: No calls. **Non-empty**: Array of call phases — see [Call Execution](#call-execution) | | `payer` | Gas payer identity. **Empty**: Sender pays. **20-byte address**: This specific payer required. See [Payer Modes](#payer-modes) | | `sender_auth` | See [Signature Format](#signature-format) | | `payer_auth` | Payer authorization. **Empty**: self-pay. **Non-empty**: `verifier || data` — same format as `sender_auth`. See [Payer Modes](#payer-modes) | #### Intrinsic Gas ``` sender_gas = AA_BASE_COST + tx_payload_cost + sender_auth_cost + nonce_key_cost + bytecode_cost + account_changes_cost + execution_gas_used total_gas_charged = sender_gas + payer_auth_cost ``` All `sender_gas` components consume `gas_limit`; intrinsic costs are charged before call execution and the remaining gas is available to `calls`. Unused gas from `gas_limit` is refunded to the payer. `payer_auth_cost` is metered separately and charged to the payer, but does not consume `gas_limit` or reduce gas available to `calls`. The sender verifier runs first, and its metered cost is included in `gas_limit`. This lets the payer sign a single maximum sender-side gas exposure without committing to the sender's verifier choice. Payer authentication is chosen by the payer and metered separately. **`sender_auth_cost`**: For EOA signatures (`sender` empty) or `ECRECOVER_VERIFIER` (`address(1)`) signatures: 6,000 gas (ecrecover + 1 SLOAD + overhead). For other configured owner signatures (`sender` set, `address(2+)` verifier): 1 SLOAD (`owner_config`) + cold code access + actual gas consumed by verifier execution. **`payer_auth_cost`**: 0 for self-pay (`payer` empty). Otherwise, the same `sender_auth_cost` model applies to the payer's verifier. | 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](./eip-2028.md). Ensures all transaction fields (`account_changes`, `sender_auth`, `calls`, 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` | | `account_changes_cost` | Per applied config change entry: auth verification cost (same model as `sender_auth_cost`) + `num_operations` × 20,000 per SSTORE. Per applied delegation entry: code deposit cost (200 × 23 bytes for the delegation indicator). Per skipped config change entry (already applied): 2,100 (SLOAD to check sequence). 0 if no config change or delegation entries in `account_changes` | #### 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 owner signature** (`sender` set): ``` verifier (20 bytes) || data ``` The first 20 bytes identify the verifier address. When the verifier is `ECRECOVER_VERIFIER`, `data` is raw ECDSA `(r || s || v)` and the protocol handles ecrecover natively. For all other verifiers, `data` is verifier-specific — each verifier defines its own wire format. ##### Validation 1. **Resolve sender**: If `sender` empty, ecrecover derives the sender address (EOA path) with `ownerId = bytes32(bytes20(sender))`. If `sender` set, read the first 20 bytes of `sender_auth` as the verifier address. 2. **Set transaction context**: Populate the Transaction Context precompile with sender, payer, and calls (see [Transaction Context](#transaction-context)). 3. **Verify**: Route by verifier address. For the EOA path (`sender` empty), ecrecover was already performed in step 1. For `ECRECOVER_VERIFIER` (`address(1)`), the protocol natively ecrecovers from `data` (as `r || s || v`), returning `ownerId = bytes32(bytes20(recovered_address))`. For all other verifiers (`address(2+)`), call `verifier.verify(hash, data)` via STATICCALL, returning `ownerId` (or `bytes32(0)` for invalid). Reject `REVOKED_VERIFIER` as a verifier address. 4. **Authorize**: SLOAD `owner_config(sender, ownerId)`. **Implicit EOA rule**: if the slot is empty, `ownerId == bytes32(bytes20(sender))`, and verification in step 3 used the native secp256k1 path (the EOA path or `ECRECOVER_VERIFIER`), treat as implicitly authorized with scope `0x00`. Otherwise, require that the stored verifier address matches the effective verifier and is not `REVOKED_VERIFIER`. 5. **Check scope**: Read the scope byte from `owner_config` (or `0x00` for the implicit case). 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`. #### 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, payer ])) ``` **Payer signature hash** — all tx fields through `calls`, excluding `payer`, `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 ])) ``` The `sender` field in the payer signature hash MUST be the resolved sender address. In the EOA path (`sender` empty in the transaction wire format), the recovered sender address (from `sender_auth` ecrecover, see [Validation](#validation) step 1) MUST be substituted into the `sender` position before computing this hash; it MUST NOT be encoded as the empty wire-format value. This binds the payer's signature to the specific resolved sender and prevents cross-sender replay of payer signatures (see [Payer Security](#security-considerations)). #### 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 `verifier || data` format as `sender_auth`: | `payer` | `payer_auth` | Payer Address | Validation | |---------|-------------|---------------|------------| | empty | empty | `sender` | Self-pay — no payer validation | | address | `verifier (20) \|\| data` | `payer` field | Sponsored — any verifier. Reads payer's `owner_config`, validates against `payer` address | Any authorized owner with SENDER scope can sign self-pay transactions. ### Account Changes The `account_changes` field is an array of typed entries for account creation and owner management: | Type | Name | Description | |------|------|-------------| | `0x00` | Create | Deploy a new account with initial owners (must be first, at most one) | | `0x01` | Config change | Owner management: authorizeOwner, revokeOwner | | `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` — there is no separate authorization field. The initial `ownerId`s for create entries are salt-committed to the derived address. Delegation requires the sender to be the account's implicit EOA owner with CONFIG scope. 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 owners 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_owners // Array of [verifier, ownerId, scope] tuples ]) ``` Initial owners are registered with their specified scope. Wallet initialization code can lock the account via `calls` in the execution phase (e.g., calling `lock()` on the Account Configuration Contract). The `code` field contains runtime bytecode placed directly at the account address. For delegation, use a delegation entry (type `0x02`) in `account_changes` after account creation. ``` [0x00, user_salt, runtimeBytecode, initial_owners] ``` ##### Address Derivation Addresses are derived using the CREATE2 address formula with the Account Configuration Contract (`ACCOUNT_CONFIG_ADDRESS`) as the deployer. The `initial_owners` are sorted by `ownerId` before hashing to ensure address derivation is order-independent (the same set of owners always produces the same address regardless of the order specified): ``` sorted_owners = sort(initial_owners, by: ownerId) owners_commitment = keccak256(ownerId_0 || verifier_0 || scope_0 || ownerId_1 || verifier_1 || scope_1 || ... || ownerId_n || verifier_n || scope_n) effective_salt = keccak256(user_salt || owners_commitment) deployment_code = DEPLOYMENT_HEADER(len(code)) || code address = keccak256(0xff || ACCOUNT_CONFIG_ADDRESS || effective_salt || keccak256(deployment_code))[12:] ``` The `owners_commitment` uses `ownerId || verifier || scope` (53 bytes) per owner — consistent with how the Account Configuration Contract identifies and configures owners. `DEPLOYMENT_HEADER(n)` is a fixed 14-byte EVM loader that returns the trailing code (see [Appendix: Deployment Header](#appendix-deployment-header) for the full opcode sequence). On non-8130 chains, `createAccount()` constructs `deployment_code` and passes it as init_code to CREATE2. On 8130 chains, the protocol constructs the same `deployment_code` for address derivation but places `code` directly (no execution). Both paths produce the same address — callers only provide `code`; the header is never user-facing. Users can receive funds at counterfactual addresses before account creation. ##### Validation (Create Entry) When a create entry is present in `account_changes`: 1. Parse `[0x00, user_salt, code, initial_owners]` where each entry is `[verifier, ownerId, scope]` 2. Reject if any duplicate `ownerId` values exist 3. Reject if `code` is empty 4. Sort by ownerId: `sorted_owners = sort(initial_owners, by: ownerId)` 5. Compute `owners_commitment = keccak256(ownerId_0 || verifier_0 || scope_0 || ... || ownerId_n || verifier_n || scope_n)` 6. Compute `effective_salt = keccak256(user_salt || owners_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 `code_size(sender) == 0` (account not yet deployed) 11. Validate `sender_auth` against one of `initial_owners` (ownerId resolved from auth must match an entry's ownerId) #### Config Change Entry Config change entries manage the account's owners. Each entry includes a `chain_id` field where `0` means valid on any chain, allowing replay across chains to synchronize owner state. ##### Config Change Format ``` rlp([ 0x01, // type: config change chain_id, // uint64: 0 = valid on any chain sequence, // uint64: monotonic ordering owner_changes, // Array of owner changes auth // Signature from an owner valid at this sequence ]) owner_change = rlp([ change_type, // uint8: operation type (see below) verifier, // address: verifier contract (authorizeOwner only) ownerId, // bytes32: owner identifier scope // uint8: permission bitmask (authorizeOwner only, 0x00 = unrestricted) ]) ``` **Operation types**: | change_type | Name | Description | Fields Used | |-------------|------|-------------|-------------| | `0x01` | `authorizeOwner` | Authorize a new owner with scope | `verifier`, `ownerId`, `scope` | | `0x02` | `revokeOwner` | Revoke an existing owner — deletes the slot for non-EOA owners; for the implicit EOA owner (`ownerId == bytes32(bytes20(account))`), overwrites with `verifier = REVOKED_VERIFIER` (`type(uint160).max`) to prevent implicit re-authorization | `ownerId` | #### 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 owner configuration *at the point after all previous entries in the list have been applied*. The authorizing owner must have CONFIG scope (see [Owner Scope](#owner-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. ##### 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("SignedOwnerChanges(address account,uint64 chainId,uint64 sequence,OwnerChange[] ownerChanges)OwnerChange(uint8 changeType,address verifier,bytes32 ownerId,uint8 scope)") ownerChangeHashes = [keccak256(abi.encode(changeType, verifier, ownerId, scope)) for each ownerChange] ownerChangesHash = keccak256(abi.encodePacked(ownerChangeHashes)) digest = keccak256(abi.encode(TYPEHASH, account, chainId, sequence, ownerChangesHash)) ``` Domain separation from transaction signatures (`AA_TX_TYPE`, `AA_PAYER_TYPE`) is structural — transaction hashes use `keccak256(type_byte || rlp([...]))`, which cannot produce the same prefix as `abi.encode(TYPEHASH, ...)`. The `auth` follows the same [Signature Format](#signature-format) as `sender_auth` (`verifier || data`), validated against the account's owner state at that point in the sequence. ##### Account Config Change Paths Owners can be modified through two portable paths: | | `account_changes` (tx field) | `applySignedOwnerChanges()` (EVM) | |--|---|---| | Authorization | Signed operation (any verifier) | Direct verification via verifier + `owner_config` | | Availability | Always (8130 chains) | Always (any chain) | | Portability | Cross-chain (chain_id 0) or chain-specific | Cross-chain (chain_id 0) or chain-specific | | Sequence | Increments channel's `change_sequence` | Increments channel's `change_sequence` | | When processed | Before code deployment (8130 only) | During EVM execution (any chain) | Both paths share the same signed owner changes and `change_sequence` counters. `applySignedOwnerChanges()` parses the verifier address from `auth`, calls the verifier to get the `ownerId`, and checks `owner_config`. Anyone can call these functions; authorization comes from the signed operation, not the caller. All owner modification paths are blocked when the account is locked (see [Account Lock](#account-lock)). #### Delegation Entry Delegation entries set [EIP-7702](./eip-7702.md)-style code delegation for the sender's account, replacing the need for an `authorization_list` in the transaction. Delegation is authorized by the transaction's `sender_auth` — no separate signature is required. The sender must be the account's implicit EOA owner (`ownerId == bytes32(bytes20(sender))`) with CONFIG scope. ##### 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 — the account's code hash is reset to the empty code hash, restoring the account to a pure EOA. On non-8130 chains, delegation uses standard [EIP-7702](./eip-7702.md) transactions (ECDSA authority). 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_owners` in Account Config storage for `sender` — for each `[verifier, ownerId, scope]` tuple, write `owner_config` (verifier address and scope byte). Initialize lock state to safe defaults: `locked = false`, `unlockDelay = 0`, `unlockRequestedAt = 0`. 2. **Config change entries** (if any): Apply operations in entry order. Reject transaction if account is locked. 3. **Delegation entries** (if any): Require the sender's resolved `ownerId == bytes32(bytes20(sender))` (EOA owner) with CONFIG scope. 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. 4. **Code placement** (if create entry present): Place `code` at `sender`. The runtime bytecode is placed directly — not executed. ### Execution #### Call Execution The protocol dispatches calls directly from `sender` to each call's `to` address: | Parameter | Value | |-----------|-------| | `from` (caller) | `sender` (the sender) | | `to` | `call.to` | | `tx.origin` | `sender` | | `msg.sender` at target | `sender` | | `msg.value` | 0 | | `data` | `call.data` | Calls carry no ETH value. ETH transfers are initiated by the account's wallet bytecode via the CALL opcode (see [Why No Value in Calls?](#why-no-value-in-calls)). Phases execute in order from a single gas pool (`gas_limit`). Within each phase, calls execute in order and are **atomic** — if any call in a phase reverts, all state changes for that phase are discarded and remaining phases are **skipped**. Completed phases **persist** — their state changes are committed and survive later phase reverts. **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. The precompile 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 | |----------|---------|-----------| | `getSender()` | `address` — the account being validated (`sender`) | Validation + Execution | | `getPayer()` | `address` — gas payer (`sender` for self-pay, payer for sponsored) | Validation + Execution | | `getOwnerId()` | `bytes32` — authenticated owner's ownerId | Execution only | | `getCalls()` | `Call[][]` — full calls array | Validation + Execution | | `getMaxCost()` | `uint256` — `gas_limit * max_fee_per_gas`, excluding separately metered `payer_auth_cost` | Validation + Execution | | `getGasLimit()` | `uint256` — sender-side gas budget (`gas_limit`) before intrinsic costs and call execution | Validation + Execution | If the wallet needs the verifier address or scope, it calls `getOwnerConfig(account, ownerId)` on the Account Configuration Contract. **Non-8130 chains**: No code at `TX_CONTEXT_ADDRESS`; STATICCALL returns zero/default values. ### Portability The system is split into storage and verification 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) | | **Verifier Contracts** | Protocol calls verifiers via STATICCALL | Same onchain contracts callable by account config contract and wallets | | **Code Delegation** | Delegation entry in `account_changes` (EOA-only authorization in this version) | Standard [EIP-7702](./eip-7702.md) transactions (ECDSA authority) | | **Transaction Context** | Precompile at `TX_CONTEXT_ADDRESS` — protocol populates, verifiers 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 owner state: a. If create entry present in `account_changes`: verify address derivation, `code_size(sender) == 0`, use `initial_owners` b. Else: read from Account Config storage 4. If config change or delegation entries present in `account_changes`: reject if account is locked (see [Account Lock](#account-lock)). For config change entries: simulate applying operations in sequence, skip already-applied entries. For delegation entries: verify `code_size(sender) == 0` or existing delegation designator. 5. Validate `sender_auth` against resulting owner state (see [Validation](#validation)). Require SENDER scope on the resolved owner. If delegation entries are present, also require `ownerId == bytes32(bytes20(sender))` (EOA owner) and CONFIG scope. 6. Resolve payer from `payer` and `payer_auth`: - `payer` empty and `payer_auth` empty: self-pay. Payer is `sender`. Reject if balance insufficient. - `payer` = 20-byte address (sponsored): `payer_auth` uses any verifier. Validate `payer_auth` against the `payer` address's `owner_config`. Require PAYER scope on the resolved owner. 7. 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 transaction hash. 8. Mempool threshold: gas payer's pending count below node-configured limits. Nodes MUST maintain a verifier allowlist that includes all verifiers in the canonical verifier set (see [Canonical Verifier Set](#canonical-verifier-set)). Nodes SHOULD NOT extend their allowlist with additional non-canonical verifiers. 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 verifier. Nodes can safely allow a higher sender rate. - **Locked payer with trusted bytecode**: A locked `payer` account whose bytecode is recognized and 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. #### 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 the sender's resolved `ownerId == bytes32(bytes20(sender))` (EOA owner) with CONFIG scope. 2. ETH gas deduction from payer (sponsor for sponsored, `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. 5. Process `account_changes` entries in order (see [Execution (Account Changes)](#execution-account-changes)). 6. Set transaction context on the Transaction Context precompile (sender, payer, ownerId, calls). 7. Execute `calls` per [Call Execution](#call-execution) semantics. Unused gas from `gas_limit` is refunded to the payer. Intrinsic gas, excluding separately metered `payer_auth_cost`, consumes `gas_limit` before call execution. For step 5, the protocol SHOULD inject log entries into the transaction receipt (e.g., `OwnerAuthorized`, `AccountCreated`, `DelegationApplied`) matching the events defined in the [IAccountConfiguration](#iaccountconfiguration) interface, following the protocol-injected log pattern established by [EIP-7708](./eip-7708.md). These protocol-injected logs are emitted only for 8130 transactions. ### RPC Extensions **`eth_getTransactionCount`**: Extended with optional `nonceKey` parameter (`uint256`) to query 2D nonce channels. Reads from the Nonce Manager precompile at `NONCE_MANAGER_ADDRESS`. **`eth_getTransactionReceipt`**: AA transaction receipts include: - `payer` (address): Gas payer address (`sender` for self-pay, specified payer for sponsored). - `status` (uint8): `0x01` = all phases succeeded (or `calls` was empty), `0x00` = one or more phases reverted. Existing tools checking `status == 1` remain correct for the success path. - `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. ### Appendix: Storage Layout The protocol reads storage directly from the Account Configuration Contract (`ACCOUNT_CONFIG_ADDRESS`) and Nonce Manager (`NONCE_MANAGER_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 Verifier Contracts? Enables signature-algorithm extension through verifier contracts. The verifier returns the `ownerId` rather than accepting it as input, so the protocol never needs algorithm-specific logic. All verifiers share a single `verify(hash, data)` interface with no type-based dispatch. Owner scope provides protocol-enforced role separation without verifier cooperation. ### 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 replay protection by transaction hash. This is useful for operations where nonce ordering coordination is undesirable. ### Why a Nonce Precompile? Nonce state is isolated in a dedicated precompile (`NONCE_MANAGER_ADDRESS`) because nonce writes occur on nearly every AA transaction, while owner config writes are relatively infrequent. The Nonce Manager has no EVM-writable state and no portability requirement — a precompile is simpler than exposing nonce mutation through the account config contract. ### Why a Transaction Context Precompile? Transaction context (sender, payer, calls, gas) is immutable transaction metadata — it never changes during execution. `ownerId` 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**: Verifiers read only what they need. Pure verifiers pay nothing for context they don't use. - **Forward compatible**: New context fields are added as new precompile functions — no interface changes to `IVerifier` or existing verifier contracts. ### 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_owners` produces the same address on any chain 2. **Pre-deployment funding**: Users can receive funds at counterfactual addresses before account creation 3. **Portability**: Same `deployment_code` produces the same address on both 8130 and non-8130 chains (see [Address Derivation](#address-derivation)) 4. **Front-running prevention**: `initial_owners` in the salt prevents attackers from deploying with different owners (see [Create Entry](#create-entry)) ### Smart Wallet Migration Path Existing ERC-4337 smart accounts migrate to native AA without redeployment: 1. **Import account**: Call `importAccount()` on the Account Configuration Contract — this verifies via the account's `isValidSignature` ([ERC-1271](./eip-1271.md)) and registers initial owners. Existing ERC-4337 wallets already implement ERC-1271, so initial owner registration works without code changes. 2. **Upgrade wallet logic**: Update contract to delegate `isValidSignature` to the Account Configuration Contract's `verifySignature()` function for owner and verifier infrastructure, and read `getOwnerId()` from the Transaction Context precompile during execution to identify which owner authorized the transaction 3. **Backwards compatible**: Wallet can still accept ERC-4337 UserOps via EntryPoint alongside native AA transactions ### 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. - **Paymaster inspection**: Verifiers can inspect calls via the Transaction Context precompile to validate payment terms. ### Why Direct Dispatch? The protocol dispatches each call directly to the specified `to` address with `msg.sender = sender`. Owners with SENDER scope are authorized to send transactions at the protocol level. Every account has wallet bytecode (via auto-delegation or explicit deployment), so calls route through the wallet for ETH-carrying operations. ### Why No Value in Calls? Since every account has wallet bytecode (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 Delegation via Account Changes? [EIP-7702](./eip-7702.md) introduced `authorization_list` as a transaction-level field for code delegation, with ECDSA authority. This proposal moves delegation into `account_changes`, authorized by the transaction's `sender_auth`. Delegation is restricted to the account's implicit EOA owner (`ownerId == bytes32(bytes20(sender))`) so that code delegation remains portable across non-8130 chains via standard [EIP-7702](./eip-7702.md) transactions. Eventually this can be expanded to all verifier types. ### Why Account Lock? Locked accounts have a frozen owner set, so the primary state that can invalidate a validated transaction is nonce consumption. This can enable nodes to cache owner state and apply higher mempool rate limits (see [Mempool Acceptance](#mempool-acceptance)). A per-owner lock alternative was considered but adds mempool tracking complexity — rate limits per `(address, ownerId)` pair rather than per address. ### Why One Slot Per Owner? The protocol reads everything it needs for authorization and scope checking in one SLOAD. Reserved bytes provide an extension path for future protocol-level owner policy. ### Why Owner Scope? Without scope, all owners have equal authority — any owner can sign as sender, approve gas payment, appear through ERC-1271, and authorize config changes. This is insufficient when accounts have owners serving different roles, like for example running a payer for ERC-20 tokens. The `0x00` = unrestricted default ensures backward compatibility. ### Why a Canonical Verifier Set? Without a required verifier set, nodes could diverge on which signature algorithms they accept beyond `ECRECOVER_VERIFIER`. 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: wallets that use canonical verifiers 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, owners are identified by `ownerId` (bytes32) and public key material is provided at signing time in the verifier-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 owner. 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. - **Simplicity**: One storage slot per owner (`owner_config`). No variable-length public key encoding, no multi-slot reads, no length fields. Registration is a single SSTORE. The protocol never needs to know how any algorithm works. ### Why bytes32 ownerId? The full 32-byte keccak256 output provides ~2^85 quantum collision resistance (vs ~2^53 for bytes20 via BHT), which is adequate for post-quantum keys. It also fits a single storage slot and aligns with keccak256 output without truncation. #### ownerId Conventions Each verifier defines how it derives `ownerId` from signature data. ## 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 owners. EOAs sending their first AA transaction are auto-delegated to `DEFAULT_ACCOUNT_ADDRESS` if they have no code. EOAs MAY override with a delegation entry in `account_changes` (EOA-only authorization), a standard [EIP-7702](./eip-7702.md) transaction, or use a create entry in `account_changes` for custom wallet implementations ## Reference Implementation ### IAccountConfiguration ```solidity interface IAccountConfiguration { struct ChangeSequences { uint64 multichain; // chain_id 0 uint64 local; // chain_id == block.chainid } struct OwnerConfig { address verifier; uint8 scopes; // 0x00 = unrestricted } struct Owner { bytes32 ownerId; OwnerConfig config; } struct OwnerChange { bytes32 ownerId; uint8 changeType; // 0x01 = authorizeOwner, 0x02 = revokeOwner bytes configData; // OwnerConfig for authorize, empty for revoke } event OwnerAuthorized(address indexed account, bytes32 indexed ownerId, OwnerConfig config); event OwnerRevoked(address indexed account, bytes32 indexed ownerId); event AccountCreated(address indexed account, bytes32 userSalt, bytes32 codeHash); event AccountImported(address indexed account); 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, Owner[] calldata initialOwners) external returns (address); function computeAddress(bytes32 userSalt, bytes calldata bytecode, Owner[] calldata initialOwners) external view returns (address); // Import existing account (ERC-1271 verification for initial owner registration) function importAccount(address account, Owner[] calldata initialOwners, bytes calldata signature) external; // Portable owner changes (direct verification via verifier + owner_config) function applySignedOwnerChanges(address account, uint64 chainId, OwnerChange[] calldata ownerChanges, bytes calldata auth) external; // Account lock (called by the account directly) function lock(uint16 unlockDelay) external; function initiateUnlock() external; // Signature verification function verifySignature(address account, bytes32 hash, bytes calldata signature) external view returns (bool verified); function verify(address account, bytes32 hash, bytes calldata auth) external view returns (uint8 scopes); // Storage views function isInitialized(address account) external view returns (bool); function isOwner(address account, bytes32 ownerId) external view returns (bool); function getOwnerConfig(address account, bytes32 ownerId) external view returns (OwnerConfig memory); function getChangeSequences(address account) external view returns (ChangeSequences memory); function isLocked(address account) external view returns (bool); function getLockStatus(address account) external view returns (bool locked, bool hasInitiatedUnlock, uint40 unlocksAt, uint16 unlockDelay); } ``` ### IVerifier ```solidity interface IVerifier { function verify( bytes32 hash, bytes calldata data ) external view returns (bytes32 ownerId); } ``` Stateful verifiers MAY read from the Transaction Context precompile or other state (see [Transaction Context](#transaction-context)). When called outside of an 8130 transaction (e.g., `verifySignature()` in a legacy transaction), the Transaction Context precompile returns zero/default values, so verifiers that depend on it naturally reject those calls. ### ITxContext (Precompile) ```solidity struct Call { address to; bytes data; } interface ITxContext { function getSender() external view returns (address); function getPayer() external view returns (address); function getOwnerId() external view returns (bytes32); function getCalls() external view returns (Call[][] memory); function getMaxCost() external view returns (uint256); function getGasLimit() external view returns (uint256); } ``` Read-only. Gas is charged as a base cost plus 3 gas per 32 bytes of returned data. ### INonceManager (Precompile) ```solidity interface INonceManager { function getNonce(address account, uint256 nonceKey) external view returns (uint64); } ``` Read-only. The protocol manages nonce storage directly; there are no state-modifying functions. ## Security Considerations **Validation Surface**: For pure verifiers, invalidators are `owner_config` revocation and nonce consumption. Stateful verifiers additionally depend on traced state; invalidation tracking is a mempool concern. **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 transaction-hash deduplication. The mempool enforces a tight expiry window (e.g., 10-30 seconds) to bound the window. Block builders MUST NOT include duplicate `NONCE_KEY_MAX` transactions with the same hash. **Owner Scope**: Protocol-enforced after verifier execution — a verifier cannot bypass scope checking. **Owner Management**: Config change authorization requires CONFIG scope. The EOA owner is implicitly authorized with unrestricted scope; revocable via portable config change. All owner 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 `ECRECOVER_VERIFIER`. Generic verifier contracts MUST NOT satisfy the implicit branch even if they return `bytes32(bytes20(sender))`, otherwise an arbitrary verifier could authenticate as any EOA whose implicit owner slot has never been written. **ownerId Binding**: The protocol checks that the verifier's returned `ownerId` maps back to that verifier in `owner_config` — preventing a malicious verifier from claiming ownership of another verifier's owners. **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 owners 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 verifier, is validated under PAYER scope, and is metered separately so the payer's verifier choice cannot reduce gas available to `calls`. **Cross-sender Payer Replay**: The payer signature hash binds to the resolved sender via the `sender` field (see [Signature Payload](#signature-payload)). In the EOA path where `sender` is empty in the wire format, the recovered sender address MUST be substituted into the `sender` position before computing the hash. Without this substitution, two different EOAs that construct otherwise identical transaction data (same `chain_id`, `nonce_key`, `nonce_sequence`, `expiry`, fees, `account_changes`, `calls`) would produce identical payer hashes, allowing a second EOA to reuse a payer signature originally issued for the first and drain the payer's gas deposit. The 2D nonce alone does not prevent this: `nonce_key` and `nonce_sequence` are fields in the transaction payload, so each attacker controls their own values. Substituting the recovered sender into the hash makes the payer's commitment per-sender and closes this replay path. The configured-owner path is unaffected because `sender` is non-empty by definition. **Account Creation Security**: `initial_owners` (verifier + ownerId + scope tuples) are salt-committed, preventing front-running of owner assignment. Wallet bytecode should be inert when uninitialized as it can be permissionlessly deployed. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).