EIP-8130 - Account Abstraction by Account Configuration

Created 2025-10-14
Status Draft
Category Core
Type Standards Track
Authors
  • Chris Hunter (@chunter-cb) <chris.hunter at coinbase.com>

Requires

Abstract

This proposal defines a new EIP-2718 transaction type and an onchain system contract that together provide account abstraction — batching, gas sponsorship, and authentication using any cryptographic system. Accounts configure signing keys with account-specified verifiers in the system contract; the protocol validates transactions natively for well-known algorithms, and via sandboxed pure-function contracts for any other scheme.

Motivation

Requiring code execution to validate transactions leads to wasted compute, denial-of-service vectors, and compensatory systems (tracing, staking, reputation). This proposal uses pure-function verifiers with known state inputs — a node can determine validity from state alone, and new signature schemes are deployed as EVM contracts without protocol changes.

This provides a native path to post-quantum secure authentication.

The system contract is accessible via the EVM, so wallet code can layer additional logic — recovery, multisig, spending limits, session keys — on top. The protocol handles authentication, gas payment, and calldata delivery to be interpreted by the account.

Specification

Constants

Name Value Comment
AA_TX_TYPE TBD EIP-2718 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
KNOWN_VERIFIER_ADDRESSES TBD Well-known verifier addresses (K1, P256, WebAuthn P256, BLS, DELEGATE)
NONCE_MANAGER_ADDRESS TBD Nonce Manager precompile address
KEY_CHANGE_TYPE TBD Magic byte for key change signature domain separation
MAX_KEY_CHANGES 10 Maximum key change entries per transaction
MAX_SIGNATURE_SIZE 2048 Maximum signature size in bytes (DoS prevention)
ENTRY_POINT_ADDRESS TBD Protocol caller address for call execution
DEPLOYMENT_HEADER_SIZE 14 Size of the deployment header in bytes

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 keys via authorizeKey() on the system contract Wallet-defined
EOAs via EIP-7702 EOAs set delegation to a smart wallet implementation, then register keys EOA key authorized by default; revocable
New Accounts (No EOA) Created via account_initialization with CREATE2 address derivation; runtime bytecode placed at address, keys + verifiers configured, call execution handles initialization Wallet-defined

Verifiers

Each key is associated with a verifier — a contract at a known address that performs signature verification. The verifier address is stored in key_config and identifies the signature algorithm. On 8130 chains, the protocol recognizes well-known verifier addresses and uses native implementations; unknown verifiers are executed in a sandboxed environment (see Sandbox Verifiers).

Well-Known Verifiers

Verifier Address Algorithm Public Key Size Signature Size keyId Derivation
K1 K1_VERIFIER secp256k1 (ECDSA) 33/65 bytes 65 bytes keccak256(x \|\| y)[12:]
P256 P256_VERIFIER secp256r1 / P-256 (raw) 33/65 bytes 64 bytes keccak256(x \|\| y)[12:]
WebAuthn P256 WEBAUTHN_P256_VERIFIER P-256 + WebAuthn framing 33/65 bytes Variable keccak256(x \|\| y)[12:]
BLS BLS_VERIFIER BLS12-381 48 bytes 96 bytes keccak256(pubkey)[12:]
DELEGATE DELEGATE_VERIFIER Delegated validation 20 bytes (address) Nested signature address (direct)

All verifiers implement the same IAuthVerifier.verify() interface (see Verifier Contracts).

DELEGATE: Delegates validation to another account's configuration. The keyId is the delegated account's address directly — a transparent pointer. Only 1 hop is permitted (see DELEGATE).

K1 / DELEGATE mutual exclusion: Because K1 keyId is an Ethereum address, a K1 key and a DELEGATE entry resolve to the same keyId when they reference the same address. They occupy the same storage slot and are therefore mutually exclusive — an account can register a K1 key for address or a DELEGATE to account, but not both. K1 pins trust to a specific private key; DELEGATE trusts any key currently authorized on the target account.

Sandbox Verifiers

Any contract can serve as a verifier if its bytecode passes a sandbox validation scan — enabling permissionless addition of new signature algorithms without protocol changes. Sandbox verifiers must be pure functions: no state access, no external calls except allowlisted precompiles. Their bytecode includes a standardized header declaring a gas_limit, which is charged as intrinsic gas. See Appendix: Sandbox Verifier Bytecode for the header format and opcode rules.

Account Configuration

Each account can authorize a set of keys through the Account Configuration Contract at ACCOUNT_CONFIG_ADDRESS. This contract handles key storage, account creation, key change sequencing, and delegates signature verification to onchain Verifier Contracts.

Keys are identified by their keyId, a 20-byte content-derived identifier (see Verifiers for per-type derivation). Only msg.sender can modify their own key configuration within EVM execution. Keys can also be modified via portable key changes (see Cross-Chain Key Changes).

Default behavior: The EOA key is implicitly authorized by default but can be disabled by revoking it on the contract.

Storage Layout

Each key occupies a key_config slot (packed: verifier address, key_policy bitfield with revoked and requireSponsor flags) plus additional slots for the public key. A per-chain key change sequence counter supports Cross-Chain Key Changes. See Appendix: Storage Layout for the full slot derivation.

The protocol validates signatures by reading key_config and public key slots directly — see Validation for the full flow. Key enumeration is performed off-chain via KeyAuthorized / KeyRevoked event logs. No key count is enforced on-chain — gas costs naturally bound key creation.

2D Nonce Storage

Nonce state is managed by a precompile at NONCE_MANAGER_ADDRESS, isolating high-frequency nonce writes from the Account Configuration Contract's key storage (see Why a Nonce Precompile?). The protocol reads and increments nonce slots directly during AA transaction processing; the precompile exposes a read-only getNonce() interface to the EVM. See Appendix: Storage Layout for slot derivation.

Verifier Contracts

Each key's verifier address in key_config determines which contract performs verification. All verifiers implement IAuthVerifier.verify() (see Reference Implementation). The Account Configuration Contract calls verifiers directly via staticcall for applyKeyChange() authorizer verification and EVM-initiated signature checks.

On 8130 chains, the protocol does not call verifier contracts for AA transaction validation — it reads key storage directly and uses native implementations for well-known verifiers or sandbox execution for unknown ones. Verifier contracts serve EVM callers only.

The DELEGATE_VERIFIER is an exception to the pure function model — it reads the delegated account's key data from the Account Configuration Contract and chains to the nested verifier, enforcing a 1-hop limit.

AA Transaction Type

A new EIP-2718 transaction with type AA_TX_TYPE:

AA_TX_TYPE || rlp([
  chain_id,
  from,               // Sender address (20 bytes) | empty for EOA signature
  nonce_key,          // 2D nonce channel (uint192)
  nonce_sequence,     // Sequence within channel (uint64)
  expiry,             // Unix timestamp (seconds)
  max_priority_fee_per_gas,
  max_fee_per_gas,
  gas_limit,
  authorization_list, 
  account_initialization,  // For creating new accounts if not using 7702 | empty
  key_changes,        // Replayable cross-chain key change operations | empty
  committed_calldata, // Calldata committed first (ie. payer required action)  | empty
  calldata,           // Calldata executed atomically | empty
  sender_auth,
  payer_auth          // 0x01 || K1 sig (65 bytes) | payer_address (20 bytes) | 0x02 || payer_address (20) || keyId (20) || sig | empty
])

Field Definitions

Field Description
chain_id Chain ID per EIP-155
from Sending account address. Required (non-empty) for configured key signatures. Empty for EOA signatures—address recovered via ecrecover. The presence or absence of from is the sole distinguisher between EOA and configured key signatures.
nonce_key 2D nonce channel key (uint192) for parallel transaction processing
nonce_sequence Must equal current sequence for (from, nonce_key). Incremented after inclusion regardless of execution outcome
expiry Unix timestamp (seconds since epoch). Transaction invalid when block.timestamp > expiry. A value of 0 means no expiry
max_priority_fee_per_gas Priority fee per gas unit (EIP-1559)
max_fee_per_gas Maximum fee per gas unit (EIP-1559)
gas_limit Maximum gas
authorization_list EIP-7702 authorization list
account_initialization Empty: No account creation. Non-empty: See Account Initialization
key_changes Empty: No key changes. Non-empty: Array of signed key change operations. See Cross-Chain Key Changes
committed_calldata Empty: No committed call. Non-empty: Calldata (bytes) delivered to from via ENTRY_POINT_ADDRESS. Committed independently — its state changes persist even if calldata execution reverts. See Call Execution
calldata Empty: No call. Non-empty: Calldata (bytes) delivered to from via ENTRY_POINT_ADDRESS. If execution reverts, its state changes are discarded. See Call Execution
sender_auth See Signature Format
payer_auth Payer authorization. Empty: self-pay. 20 bytes: delegate payer (payer_address). other: verifier based sponsor. See Payer Modes

Intrinsic Gas

intrinsic_gas = AA_BASE_COST + tx_payload_cost + sender_key_cost + payer_cost + nonce_key_cost + bytecode_cost + key_changes_cost

sender_key_cost: Determined by the verifier address read from the sender's key_config:

Verifier Gas Rationale
EOA (no key_config) 6000 ecrecover (3000) + 1 SLOAD (key_config) + overhead
K1_VERIFIER 6000 ecrecover (3000) + 1 SLOAD (key_config) + overhead
P256_VERIFIER 7000 P256 verify + reads public key
WEBAUTHN_P256_VERIFIER 12000 + calldata_gas P256 verify + WebAuthn parsing + reads public key
BLS_VERIFIER 8000 BLS verify + reads public key
DELEGATE_VERIFIER 3000 + nested 1 SLOAD + nested sig cost
Sandbox verifier declared gas_limit + reads public key Gas from bytecode header, charged in full

All types read key_config (1 SLOAD) for authorization, policy checks, and verifier address. Non-K1 types require additional SLOADs to read the public key.

payer_cost: Determined by the payer mode (see Payer Modes):

Payer Mode Gas Rationale
Self-pay 0 key_policy.requireSponsor checked from sender validation SLOAD (no additional read)
Delegate payer 2,100 1 cold SLOAD (delegate key verification)
K1 sponsor 3,000 ecrecover (no storage read)
Verifier sponsor sender_key_cost for payer's verifier Reads payer's key_config + public key, runs verification
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_initialization, key_changes, authorization_list, sender_auth, committed_calldata, calldata, etc.) are charged for data availability
nonce_key_cost 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 account_initialization empty. Otherwise: 32,000 (deployment base) + code deposit cost (200 gas per deployed byte). Byte costs for bytecode are covered by tx_payload_cost
key_changes_cost Per applied entry: authorizer signature verification cost (based on authorizer's verifier, using the sender_key_cost table above) + num_operations × 20,000 per SSTORE for key struct slots. Per skipped entry (already applied): 2,100 (SLOAD to check sequence). 0 if key_changes empty

Signature Format

Signature format is determined by the from field:

EOA signature (from empty): Raw 65-byte ECDSA signature (r || s || v). The sender address is recovered via ecrecover. This is the only format that uses key recovery.

Configured key signature (from set):

keyId (20 bytes) || signature_data
Verifier Signature Format Total Size Protocol Reads
K1 keyId (20) \|\| r,s,v (65) 85 bytes 1 SLOAD
P256 keyId (20) \|\| r,s (64) 84 bytes reads key_config + public key
WebAuthn P256 keyId (20) \|\| authenticatorData \|\| cDJ_len (2) \|\| clientDataJSON \|\| r,s (64) Variable reads key_config + public key
BLS keyId (20) \|\| sig (96) 116 bytes reads key_config + public key
DELEGATE delegate_address (20) \|\| nested_signature Variable 1 SLOAD + nested
Sandbox keyId (20) \|\| verifier-specific data Variable reads key_config + public key + sandbox gas

All configured key signatures begin with keyId (20 bytes). The protocol reads key_config for the keyId, which yields the verifier address and determines how to parse the remaining signature_data. No auth type byte is needed — the verifier address in storage defines the algorithm.

Validation
  1. Parse keyId: First 20 bytes of configured key signature. For EOA (from empty): ecrecover derives the sender address directly — skip remaining steps.
  2. Read key_config for (from, keyId) (1 SLOAD): yields verifier and key_policy. Verify authorized — non-zero verifier + not revoked. For keyId == from with empty slot: EOA default (valid with K1_VERIFIER); revoked set = blocked.
  3. Read public key from storage. Verify signature: for well-known verifiers, use native implementation; for unknown verifiers, sandbox execution with declared gas limit. Reject if verification fails.
DELEGATE

For DELEGATE_VERIFIER, the protocol reads the delegated account's address from the publicKey field (stored as 20 bytes), then parses nested_signature as nested_keyId (20) || nested_sig_data. It reads the nested key's key_config from the delegated account and validates using the nested verifier. Only 1 hop is permitted; nested DELEGATE_VERIFIER results in an immediate mempool drop.

Example (Account B delegates to Account A, which has a P256 key):

Account_A_address (20) || A_P256_keyId (20) || r,s (64)

Signature Payload

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

Sender signature hash (the marker byte in the last position commits to a payer mode — see Payer Modes):

// Self-pay (no sponsor):
keccak256(AA_TX_TYPE || rlp([
  chain_id, from, nonce_key, nonce_sequence, expiry,
  max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
  authorization_list, account_initialization, key_changes, committed_calldata, calldata,
  0x80
]))

// Open sponsor (any payer):
keccak256(AA_TX_TYPE || rlp([
  chain_id, from, nonce_key, nonce_sequence, expiry,
  max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
  authorization_list, account_initialization, key_changes, committed_calldata, calldata,
  0x00
]))

// Committed sponsor (specific payer):
keccak256(AA_TX_TYPE || rlp([
  chain_id, from, nonce_key, nonce_sequence, expiry,
  max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
  authorization_list, account_initialization, key_changes, committed_calldata, calldata,
  payer_address
]))

Payer signature hash (when sponsoring):

keccak256(AA_PAYER_TYPE || rlp([
  chain_id, from, nonce_key, nonce_sequence, expiry,
  max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
  authorization_list, account_initialization, key_changes, committed_calldata, calldata
]))

Payer Modes

Gas payment mode is disambiguated by length and leading byte of payer_auth:

Mode Sender signs payer_auth (wire) Gas Payer Description
Self-pay 0x80 (empty) empty from Rejected if key_policy.requireSponsor set.
Delegate payer <payer_address> payer_address (20) payer_address Standing authorization via on-chain DELEGATE key. No payer signature.
K1 sponsor <payer_address> or 0x00 0x01 \|\| K1_signature (65) Payer (ecrecover) Committed or open sponsorship.
Verifier sponsor <payer_address> or 0x00 0x02 \|\| payer_address (20) \|\| keyId (20) \|\| sig_data payer_address Payer authenticates via any registered verifier.

Delegate payer: The payer has a DELEGATE key (verifier == DELEGATE_VERIFIER) for keyId == from, granting standing authorization for the sender to use their ETH for gas. The on-chain DELEGATE key replaces a per-transaction payer signature. The sender always commits to a specific payer_address (no open delegate mode).

Verifier sponsor: The payer authenticates using any key registered on their own Account Configuration. The protocol reads the payer's key_config for the given keyId and verifies via the stored verifier — same flow as sender validation.

Combined with per-key requireSponsor (see Storage Layout), this enables a "gas piggy bank" pattern: keys that cannot self-pay draw gas from a dedicated funding account. One gas account can fund multiple accounts by registering a DELEGATE key for each, fully isolating gas spend from the accounts' own ETH balances.

Account Initialization

New smart contract accounts can be created with pre-configured keys in a single transaction using the account_initialization field. The bytecode is the runtime code placed directly at the account address — it is not executed during deployment. The account's initialization logic runs when calldata is delivered to the account via the execution phase that follows:

account_initialization = rlp([
  user_salt,          // bytes32: User-chosen uniqueness factor
  bytecode,           // bytes: Runtime bytecode placed directly at the account address
  initial_keys        // Array of [verifier, public_key] pairs
])

Address Derivation

Addresses are derived using the CREATE2 address formula with the Account Configuration Contract (ACCOUNT_CONFIG_ADDRESS) as the deployer. The initial_keys are sorted by keyId before hashing to ensure address derivation is order-independent (the same set of keys always produces the same address regardless of the order specified):

sorted_keys = sort(initial_keys, by: keyId)

keys_commitment = keccak256(keyId_0 || verifier_0 || keyId_1 || verifier_1 || ... || keyId_n || verifier_n)

effective_salt = keccak256(user_salt || keys_commitment)
deployment_code = DEPLOYMENT_HEADER(len(bytecode)) || bytecode
address = keccak256(0xff || ACCOUNT_CONFIG_ADDRESS || effective_salt || keccak256(deployment_code))[12:]

The keys_commitment uses keyId || verifier (40 bytes) per key — consistent with how the Account Configuration Contract identifies keys.

DEPLOYMENT_HEADER(n) is a fixed 14-byte EVM loader that returns the trailing bytecode (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 bytecode directly (no execution). Both paths produce the same address — callers only provide bytecode; the header is never user-facing.

Users can receive funds at counterfactual addresses before account creation.

Validation (Account Initialization)

When account_initialization is non-empty:

  1. Parse [user_salt, bytecode, initial_keys]
  2. For each key in initial_keys, derive keyId. For compressed public keys, decompress to the uncompressed point before derivation.
  3. Reject if any duplicate keyId values exist
  4. Sort by keyId: sorted_keys = sort(initial_keys, by: keyId)
  5. Compute keys_commitment = keccak256(keyId_0 || verifier_0 || ... || keyId_n || verifier_n)
  6. Compute effective_salt = keccak256(user_salt || keys_commitment)
  7. Compute deployment_code = DEPLOYMENT_HEADER(len(bytecode)) || bytecode
  8. Compute expected = keccak256(0xff || ACCOUNT_CONFIG_ADDRESS || effective_salt || keccak256(deployment_code))[12:]
  9. Require from == expected
  10. Require code_size(from) == 0 (account not yet deployed)
  11. Validate sender_auth against one of initial_keys (keyId extracted from signature must match an entry's computed keyId)

Execution (Account Initialization)

  1. Register initial_keys in Account Config storage for from: for each key, compute keyId, write verifier and public_key to the key struct slots
  2. Apply key_changes (if non-empty) — see Cross-Chain Key Changes
  3. Place bytecode at from
  4. Proceed with call execution

Key registration and key changes are applied before code placement so that the account's initialization logic (executed via call execution) can read its own key configuration from the Account Config contract.

Cross-Chain Key Changes

The key_changes field enables cross-chain portable key management. Key change operations include a chain_id field where 0 means valid on any chain, allowing them to be replayed across chains to synchronize key state.

Key Change Format

key_changes = [
  rlp([
    chain_id,           // uint64: 0 = valid on any chain
    sequence,           // uint64: monotonic ordering
    operations,         // Array of key operations
    authorizer_auth     // Signature from a key valid at this sequence
  ]),
  ...  // max MAX_KEY_CHANGES entries per transaction
]

operation = rlp([
  op_type,              // 0x01 = authorizeKey, 0x02 = revokeKey
  verifier,             // address: verifier contract address
  public_key            // bytes: public key material
])

Each entry represents a set of key operations authorized at a specific sequence number. The authorizer_auth must be valid against the account's key configuration at the point after all previous entries in the list have been applied.

The sequence number is scoped to a 2D channel defined by the chain_id to prevent cross-chain sequence de-sync. A chain_id of 0 uses the multichain sequence channel, while a specific chain_id uses that chain's local sequence channel.

Key Change Signature Payload

Key change signatures use domain separation via KEY_CHANGE_TYPE. The chain_id field controls replay scope: 0 means the key change is valid on any chain, otherwise it is only valid on the specified chain.

keccak256(KEY_CHANGE_TYPE || rlp([
  from,
  chain_id,
  sequence,
  operations
]))

The authorizer_auth follows the same Signature Format as sender_auth (EOA or configured key), validated against the account's key state at that point in the sequence.

Key Change Paths

Keys can be modified through three paths:

Portable: key_changes (tx field) Portable: applyKeyChange() (EVM) Local: authorizeKey() / revokeKey() (EVM)
Authorization Signed operation (any verifier) Signed operation (any verifier) msg.sender during execution
Portability Cross-chain (chain_id 0) or chain-specific Cross-chain (chain_id 0) or chain-specific Chain-local only
Sequence Increments channel's key_change_sequence Increments channel's key_change_sequence Does NOT affect key_change_sequence
When processed Before code deployment (8130 only) During EVM execution (any chain) During EVM execution

Both portable paths share the same signed operations and key_change_sequence counters. applyKeyChange() verifies the authorizer via IAuthVerifier(verifier).verify(...) — anyone (including relayers) can call it; authorization comes from the cryptographic signature, not the caller.

Local key changes (authorizeKey() / revokeKey()) require msg.sender and do not increment key_change_sequence. Portable key changes may overwrite local changes at the same keyIds.

Execution

Call Execution

Both committed_calldata and calldata are delivered to from as individual calls:

Parameter Value
to from
tx.origin from
msg.sender ENTRY_POINT_ADDRESS
msg.value 0
data committed_calldata or calldata

Execution proceeds in two phases:

Both phases share a single gas pool from gas_limit. committed_calldata executes first; calldata receives the remainder.

  1. Committed phase: If committed_calldata is non-empty, execute a call to from. This call is committed independently — its state changes persist regardless of whether the calldata phase succeeds or reverts. If committed_calldata reverts, its state changes are discarded and execution continues to the next phase. For accounts without code, the call succeeds with no effect.

  2. Calldata phase: If calldata is non-empty, execute a call to from. If execution reverts, its state changes are discarded. For accounts without code, the call succeeds with no effect.

The wallet fully interprets both payloads — batching, multicall, or any other execution pattern is handled by the wallet's code, not the protocol. This two-phase design enables robust token gas payments: the wallet transfers tokens to a sponsor in committed_calldata, while the user's operations execute in calldata. The sponsor's payment survives even if calldata reverts.

Transaction Context

During AA transaction execution, accounts can query the Account Configuration Contract for the current transaction's authorization context:

The protocol injects this context using EIP-1153 transient storage (TSTORE) on the Account Configuration Contract before call execution. Only two values are written:

Slot Value Size
keccak256("context.payer") Payer address 20 bytes
keccak256("context.signer") Signer keyId 20 bytes

getCurrentPayer() reads payer via TLOAD. getCurrentSigner() reads keyId via TLOAD, then looks up (keyConfig, publicKey) from persistent key storage. Transient storage is automatically cleared at transaction end.

Non-8130 chains: These functions return zero/default values since no protocol writes to transient storage.

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 uses native implementations for well-known verifiers; sandbox for unknown Same onchain contracts callable by account config contract and wallets
Nonce Manager Precompile at NONCE_MANAGER_ADDRESS Not applicable; nonce management by existing systems (e.g., ERC-4337 EntryPoint)

The Account Configuration Contract is identical Solidity bytecode on every chain (deployed via CREATE2). Verifier contracts are also deployed at deterministic addresses. See Why Verifier Contracts? for the design rationale.

Validation Flow

Mempool Acceptance

  1. Verify sender_auth size ≤ MAX_SIGNATURE_SIZE, key_changes length ≤ MAX_KEY_CHANGES
  2. Resolve sender: if from set, use it; if empty, ecrecover from sender_auth
  3. Determine effective key state: a. If account_initialization non-empty: verify address derivation, code_size(from) == 0, use initial_keys b. Else: read from Account Config storage
  4. If key_changes non-empty: simulate applying operations in sequence, skip already-applied entries
  5. Validate sender_auth against resulting key state (see Validation)
  6. Resolve payer from payer_auth:
  7. Empty: reject if key_policy.requireSponsor set. Payer is from.
  8. 20 bytes: Delegate payer. Verify payer has DELEGATE key for sender. Payer is payer_address.
  9. 0x01 prefix: K1 sponsor. Recover payer via ecrecover over the trailing 65-byte signature. Verify sender signed <payer_address> or 0x00.
  10. 0x02 prefix: Verifier sponsor. Parse payer_address || keyId || sig_data. Read payer's key_config, verify via stored verifier. Verify sender signed <payer_address> or 0x00.
  11. Verify nonce, payer ETH balance, expiry
  12. Mempool threshold: payer's pending sponsored count below node-configured limits

Block Execution

  1. ETH gas deduction from payer (unused gas is refunded to the payer)
  2. Increment nonce in Nonce Manager storage
  3. Process authorization_list (EIP-7702)
  4. Register initial_keys in Account Config storage (if account_initialization non-empty)
  5. Apply key_changes to Account Config storage (if non-empty)
  6. Place bytecode at from (if account_initialization non-empty; code is placed directly, not executed)
  7. Write transaction context to Account Config transient storage via TSTORE (payer address, signer keyId)
  8. Execute committed_calldata via ENTRY_POINT_ADDRESS if non-empty (committed independently)
  9. Execute calldata via ENTRY_POINT_ADDRESS if non-empty

Steps 1–7 are protocol-level direct state operations with no EVM execution. Steps 8–9 are the EVM execution phase. Following the precedent of EIP-7702, the protocol-level state changes applied in steps 1–7 MUST NOT be reverted if the EVM execution in steps 8–9 reverts. committed_calldata in step 8 is committed independently — its state changes MUST NOT be reverted if step 9 reverts. For steps 4–6, the protocol SHOULD inject log entries into the transaction receipt (e.g., KeyAuthorized, AccountCreated) matching the events defined in the IAccountConfig interface, following the protocol-injected log pattern established by EIP-7708. This ensures indexers observe the same events regardless of whether keys are registered via protocol (8130) or via EVM calls (non-8130 chains).

RPC Extensions

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

eth_getTransactionReceipt: Should include a payer field with the gas payer address.

Appendix: Storage Layout

Account Configuration Contract (ACCOUNT_CONFIG_ADDRESS):

Base slot: keccak256(account_address || ACCOUNT_CONFIG_ADDRESS)

Key struct (per keyId):
  keccak256(keyId || base_slot) + 0: key_config (uint256, packed)
    bytes 0-19: verifier (address)  non-zero + not revoked = authorized
    byte 20: key_policy (uint8, bitfield)
      bit 0: revoked  key explicitly revoked;
      bit 1: requireSponsor  this key requires gas sponsorship
      bits 2-7: reserved
    bytes 21-31: reserved
  keccak256(keyId || base_slot) + 1: public_key_length (uint32)
  keccak256(keyId || base_slot) + 2: public_key bytes [0:32]
  keccak256(keyId || base_slot) + 3: public_key bytes [32:64] (if needed)
  ...additional slots as needed: ceil(len(public_key) / 32) slots total

Key change sequence:
  keccak256(base_slot || "key_sequence" || chain_id): latest applied sequence (uint64)
    chain_id 0 = multichain channel, specific chain_id = chain-local channel

Nonce Manager Precompile (NONCE_MANAGER_ADDRESS):

Base slot: keccak256(account_address || NONCE_MANAGER_ADDRESS)
Nonce slot: base_slot + nonce_key
Value: current_sequence (uint64)

Appendix: Sandbox Verifier Bytecode

Sandbox verifier bytecode must include a standardized header and contain no forbidden opcodes:

Bytecode header:
  Byte 0:    0x60         PUSH1
  Byte 1:    0x08         (jump offset)
  Byte 2:    0x56         JUMP
  Byte 3-4:  0x81 0x30    magic ("8130")
  Byte 5-7:  gas_limit    uint24, units of 1k gas 
  Byte 8:    0x5B         JUMPDEST
  Byte 9+:   verification code

Allowed opcodes: Stack operations, arithmetic, bitwise, KECCAK256, memory operations, CALLDATALOAD/SIZE/COPY, RETURN, REVERT, jumps, and STATICCALL to allowlisted precompile addresses only.

Forbidden opcodes: CALL, DELEGATECALL, CALLCODE, SLOAD, SSTORE, TLOAD, TSTORE, all external state reads (BALANCE, EXTCODESIZE, etc.), CREATE, CREATE2, SELFDESTRUCT, LOG0–LOG4.

STATICCALL is allowed in bytecode but runtime-filtered: the target address must be an allowlisted precompile. This enables verifiers to use existing precompiles (modexp, SHA-256, ecrecover, etc.) as building blocks while maintaining the pure function guarantee.

Appendix: Deployment Header

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

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

Rationale

Why Verifier Contracts?

Key storage and signature verification have fundamentally different upgrade characteristics. Storage is mechanical — (verifier, public_key) in deterministic slots works on any EVM chain. But if verification logic were bundled into the storage contract, it would freeze at deploy time with no permissionless upgrade path.

Separating them means new algorithms are deployed as new verifier contracts without touching the storage layer. The uniform IAuthVerifier.verify() interface eliminates registration, wallet callbacks, and special casing — the verifier address in key_config is the sole identifier for authentication logic. The storage contract is pure Solidity (SLOAD/SSTORE-dominated, no gas benefit from a precompile).

Storage layout is consensus-critical: Because the protocol reads storage slots directly on 8130 chains, the keccak-derived slot layout becomes a consensus rule. The layout is intentionally simple (keccak-derived, fixed-size slots per key) to minimize the likelihood of future changes — the same commitment that EIP-7702 makes with its delegation designator format.

Why a Nonce Precompile?

Nonce state is isolated in a dedicated precompile (NONCE_MANAGER_ADDRESS) rather than stored alongside key configurations in the Account Configuration Contract. This separation is motivated by their fundamentally different access patterns and portability requirements:

Property Key Config Nonces
Write frequency Rare (key rotation) Every AA transaction
Read frequency Every validation Every validation
Growth Rare (gas-bounded) Unbounded (nonce channels)
EVM writes Yes (authorizeKey, revokeKey, etc.) No (protocol-only increments)
Portability Required (for non 8130 chains) Not required (8130-only)

Why a precompile instead of a system contract? Unlike the Account Configuration Contract — which must be a full Solidity contract for cross-chain portability and EVM-writable key management — the Nonce Manager has no EVM-writable state and no portability requirement. Nonce increments are exclusively protocol-level operations.

Modularity: The precompile is minimal — a single read function backed by protocol-managed storage. This clean separation means nonce logic can evolve independently, and the precompile could potentially be reused by other transaction types or systems.

Why CREATE2 for Account Initialization?

Account initialization uses the CREATE2 address formula with ACCOUNT_CONFIG_ADDRESS as the deployer address for cross-chain portability:

  1. Deterministic addresses: Same user_salt + bytecode + initial_keys produces the same address on any chain
  2. Pre-deployment funding: Users can receive funds at counterfactual addresses before account creation
  3. Portability: Same deployment_code produces the same address on both 8130 and non-8130 chains (see Address Derivation)
  4. Front-running prevention: initial_keys in the salt prevents attackers from deploying with different keys (see Account Initialization)

Smart Wallet Migration Path

Existing ERC-4337 smart accounts migrate to native AA without redeployment:

  1. Register keys: Call authorizeKey() on the Account Configuration Contract to authorize existing signing keys (K1, P256, etc.)
  2. Upgrade wallet logic: Update contract to delegate isValidSignature to the appropriate verifier contract, and call getCurrentSigner() during execution to identify which key authorized the transaction
  3. Backwards compatible: Wallet can still accept ERC-4337 UserOps via EntryPoint alongside native AA transactions

Why Verifier Addresses Instead of Auth Type Bytes?

Earlier designs used a 1-byte auth_type to identify signature algorithms. This created a protocol-managed registry of algorithms that required hard forks to extend. The verifier address model replaces this with a permissionless system:

Why Two-Phase Execution (committed_calldata + calldata)?

The two-phase split guarantees sponsor payment survives regardless of what happens in calldata. This enables robust permissioned sponsorship in exchange for gas payments without protocol-level token awareness. The wallet fully interprets both payloads — batching, multicall, or any other execution pattern is the wallet's responsibility, not the protocol's.

Why Key Policy?

The key_config slot packs verifier (20 bytes) and key_policy (1 byte) into the same 32-byte storage slot, read in a single SLOAD during sender validation — zero additional storage cost for policy enforcement.

The revoked flag enables cross-chain portable EOA key revocation via the existing revokeKey key change mechanism. The requireSponsor flag provides protocol-enforced session key semantics — a key that can authenticate but cannot access the account's ETH for gas. This restriction is enforced at the protocol layer before any EVM execution. Wallet code can layer additional restrictions (allowed targets, spending caps) by checking getCurrentSigner() during execution, but the ETH-access guarantee comes from the protocol. This limits the blast radius of a compromised session key to whatever the wallet permits, never the account's raw ETH balance.

Future Signature Algorithms

The verifier model is designed for permissionless extensibility. New algorithms (post-quantum schemes like ML-DSA, ZK-proof-based auth, exotic curves) are deployed as sandbox verifier contracts — no protocol change required. Because sandbox verifiers are pure functions with declared gas limits, they fit the protocol's validation model without reintroducing arbitrary EVM execution. Well-known verifiers (K1, P256, BLS) use native implementations for optimal gas; any future algorithm that becomes widely adopted can be promoted to a well-known verifier via protocol upgrade.

Backwards Compatibility

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

Reference Implementation

IAccountConfig

interface IAccountConfig {
    struct AuthKey {
        address verifier;
        bytes publicKey;
    }

    struct KeyOperation {
        uint8 opType;       // 0x01 = authorizeKey, 0x02 = revokeKey
        address verifier;
        bytes publicKey;
    }

    event KeyAuthorized(address indexed account, bytes20 indexed keyId, address verifier, uint8 keyPolicy, bytes publicKey);
    event KeyRevoked(address indexed account, bytes20 indexed keyId);
    event KeyPolicyChanged(address indexed account, bytes20 indexed keyId, uint8 keyPolicy);
    event AccountCreated(address indexed account, bytes32 userSalt, bytes32 codeHash);
    event KeyChangeApplied(address indexed account, uint64 sequence);

    // Account creation (factory)
    function createAccount(bytes32 userSalt, bytes calldata bytecode, AuthKey[] calldata initialKeys) external returns (address);
    function getAddress(bytes32 userSalt, bytes calldata bytecode, AuthKey[] calldata initialKeys) external view returns (address);

    // Key management (msg.sender only)
    function authorizeKey(address verifier, bytes calldata publicKey) external returns (bytes20 keyId);
    function revokeKey(bytes20 keyId) external;
    function setKeyPolicy(bytes20 keyId, uint8 keyPolicy) external;

    // Portable key changes (calls IAuthVerifier(verifier).verify() for authorizer)
    function applyKeyChange(address account, uint64 chainId, uint64 sequence, KeyOperation[] calldata operations, bytes calldata authorizerAuth) external;
    function getKeyChangeSequence(address account, uint64 chainId) external view returns (uint64);

    // Read functions
    function isAuthorized(address account, bytes20 keyId) external view returns (bool);
    function getKeyData(address account, bytes20 keyId) external view returns (bytes32 keyConfig, bytes memory publicKey);

    // Transaction context (8130 only — reads from transient storage)
    function getCurrentPayer() external view returns (address);
    function getCurrentSigner() external view returns (bytes20 keyId, bytes32 keyConfig, bytes memory publicKey);
}

IAuthVerifier

interface IAuthVerifier {
    function verify(
        address account,
        bytes20 keyId,
        bytes calldata publicKey,
        bytes32 hash,
        bytes calldata signature
    ) external view returns (bool);
}

INonceManager (Precompile)

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

Read-only. The protocol manages nonce storage directly; there are no state-modifying functions.

Security Considerations

Enshrined Validation: Well-known verifiers use native implementations before any EVM execution. Sandbox verifiers execute as pure functions with declared gas limits and no state access — deterministic results eliminate invalidation-based DoS without requiring a reputation system. Failed validation rejects transactions before mempool entry.

Replay Protection: Transactions include chain_id, 2D nonce, and expiry.

Key Management: Only msg.sender can modify keys via local EVM calls; portable key changes require signature authorization. EOA key is implicitly authorized by default; revocable via revokeKey (cross-chain portable). Accounts SHOULD have at least one configured key before revoking the EOA key. The requireSponsor key policy limits session key blast radius by preventing gas access. MAX_KEY_CHANGES bounds per-transaction processing cost; KEY_CHANGE_TYPE domain separation prevents authorizer signatures from being reused as transaction signatures.

Delegation: See DELEGATE for hop limits and nested signature rules. The DELEGATE verifier enforces a 1-hop limit at both protocol and contract level.

Payer Security: AA_TX_TYPE vs AA_PAYER_TYPE domain separation prevents signature reuse between sender and payer roles. The sender's signed marker commits to a payer mode. Revoking a DELEGATE key immediately invalidates all pending transactions using that payer.

Signature Size Limits: Signatures exceeding MAX_SIGNATURE_SIZE MUST be rejected to prevent DoS via oversized signatures.

Account Initialization Security: initial_keys are salt-committed, preventing front-running. Permissionless deployment via createAccount() is safe — even if front-run, the account is created with the owner's keys. Wallet bytecode should be inert when uninitialized.

Copyright

Copyright and related rights waived via CC0.