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.
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.
| 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 |
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 |
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).
| 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.
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.
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.
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.
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.
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.
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 | 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 = 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 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.
from empty): ecrecover derives the sender address directly — skip remaining steps.(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.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)
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
]))
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.
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
])
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.
When account_initialization is non-empty:
[user_salt, bytecode, initial_keys]initial_keys, derive keyId. For compressed public keys, decompress to the uncompressed point before derivation.keyId values existsorted_keys = sort(initial_keys, by: keyId)keys_commitment = keccak256(keyId_0 || verifier_0 || ... || keyId_n || verifier_n)effective_salt = keccak256(user_salt || keys_commitment)deployment_code = DEPLOYMENT_HEADER(len(bytecode)) || bytecodeexpected = keccak256(0xff || ACCOUNT_CONFIG_ADDRESS || effective_salt || keccak256(deployment_code))[12:]from == expectedcode_size(from) == 0 (account not yet deployed)sender_auth against one of initial_keys (keyId extracted from signature must match an entry's computed keyId)initial_keys in Account Config storage for from: for each key, compute keyId, write verifier and public_key to the key struct slotskey_changes (if non-empty) — see Cross-Chain Key Changesbytecode at fromKey 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.
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_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 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.
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.
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.
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.
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.
During AA transaction execution, accounts can query the Account Configuration Contract for the current transaction's authorization context:
getCurrentPayer() returns the gas payer address (from for self-pay, recovered payer for sponsored)getCurrentSigner() returns (keyId, keyConfig, publicKey) of the key that authorized the transactionThe 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.
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.
sender_auth size ≤ MAX_SIGNATURE_SIZE, key_changes length ≤ MAX_KEY_CHANGESfrom set, use it; if empty, ecrecover from sender_authaccount_initialization non-empty: verify address derivation, code_size(from) == 0, use initial_keys
b. Else: read from Account Config storagekey_changes non-empty: simulate applying operations in sequence, skip already-applied entriessender_auth against resulting key state (see Validation)payer_auth:key_policy.requireSponsor set. Payer is from.payer_address.0x01 prefix: K1 sponsor. Recover payer via ecrecover over the trailing 65-byte signature. Verify sender signed <payer_address> or 0x00.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.initial_keys in Account Config storage (if account_initialization non-empty)key_changes to Account Config storage (if non-empty)bytecode at from (if account_initialization non-empty; code is placed directly, not executed)committed_calldata via ENTRY_POINT_ADDRESS if non-empty (committed independently)calldata via ENTRY_POINT_ADDRESS if non-emptySteps 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).
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.
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)
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.
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)
]
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.
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.
Account initialization uses the CREATE2 address formula with ACCOUNT_CONFIG_ADDRESS as the deployer address for cross-chain portability:
user_salt + bytecode + initial_keys produces the same address on any chaindeployment_code produces the same address on both 8130 and non-8130 chains (see Address Derivation)initial_keys in the salt prevents attackers from deploying with different keys (see Account Initialization)Existing ERC-4337 smart accounts migrate to native AA without redeployment:
authorizeKey() on the Account Configuration Contract to authorize existing signing keys (K1, P256, etc.)isValidSignature to the appropriate verifier contract, and call getCurrentSigner() during execution to identify which key authorized the transactionEarlier 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:
keyId || signature_data format. The verifier address in storage determines parsing.IAuthVerifier.verify(), whether ecrecover, P256, BLS, or a post-quantum scheme.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.
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.
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.
No breaking changes. Existing EOAs and smart contracts function unchanged. Adoption is opt-in:
account_initialization) to interpret calldata delivered by the protocolinterface 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);
}
interface IAuthVerifier {
function verify(
address account,
bytes20 keyId,
bytes calldata publicKey,
bytes32 hash,
bytes calldata signature
) external view returns (bool);
}
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.
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 and related rights waived via CC0.