--- eip: 8401 title: Portable Account Keystore description: Define portable account actors and authenticators author: Chris Hunter (@chunter-cb) , Pedro Gomes (@pedrouid) discussions-to: https://ethereum-magicians.org/t/composable-native-account-abstraction/29526 status: Draft type: Standards Track category: Core created: 2026-08-27 requires: 155, 712, 1014, 1271, 6780 --- ## Abstract This proposal specifies a portable Keystore for smart accounts. The Keystore stores account actors, binds each actor to an authenticator contract and scope word, supports local and multichain configuration changes, and provides deterministic account creation and import. Its contract interface works on any EVM chain and can be used by native account-abstraction transactions, [ERC-4337](./eip-4337.md), or other execution transports. ## Motivation Account authority should not depend on one transaction transport. A wallet may use an ERC-4337 UserOperation on one chain, a native account-abstraction transaction on another, and a future transport elsewhere while retaining the same account address, actors, authenticators, and signed configuration changes. Separating the Keystore from transaction processing gives every transport a common authority layer. Chains may deploy the Keystore as an ordinary contract, while protocol integrations may read the same storage and apply the same operations natively without changing their results. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. ### Overview The Keystore is deployed at `KEYSTORE_ADDRESS`. Each account authorizes actors through the Keystore. An actor is identified by a `bytes32 actorId`, bound to one authenticator, and assigned an expiry and a `uint16` scope word. An authenticator verifies transport-defined hashes and returns the authenticated `actorId`. The Keystore then resolves the actor configuration and returns the actor's scope to the caller. Consumers define which non-zero scope bits authorize their operations. The Keystore provides four portable functions: 1. Store account authority independently from account code. 2. Authenticate actors through replaceable authenticator contracts. 3. Apply signed local or multichain authority changes. 4. create or import accounts with deterministic initial authority. ### Actors and Authenticators An actor configuration contains: ```text authenticator (20 bytes) || expiry (6 bytes) || scope (2 bytes) || reserved (4 bytes) ``` The fields have the following meanings: | Field | Meaning | |---|---| | `authenticator` | Contract or standardized authenticator that resolves the actor | | `expiry` | Unix timestamp in seconds; zero means no expiry | | `scope` | Consumer-defined grants; zero means admin | | `reserved` | Version gate; MUST be zero | An actor is live when: ```text expiry == 0 || block.timestamp <= expiry ``` A non-zero `actorId` MAY be authorized only when its authenticator is non-zero. Revocation deletes the complete actor configuration. #### Scope Semantics Admin authority is exactly `scope == 0x0000`. Only an admin actor may authorize Keystore configuration changes. For `scope != 0`, every set bit is a grant interpreted by the consuming standard. This proposal assigns no non-zero scope bits. Consumers MUST treat unknown bits as granting no authority, and future assignments MUST be pure grants. The Keystore stores the complete scope word verbatim. It MUST NOT reject combinations merely because a deployed consumer does not understand them. #### Authenticator Interface An authenticator implements: ```solidity interface IAuthenticator { function authenticate( bytes32 hash, bytes calldata data ) external view returns (bytes32 actorId); } ``` The authenticator MUST return `bytes32(0)` for invalid authentication or revert. A caller MUST treat both outcomes as invalid. The Keystore MUST verify that the returned `actorId` is bound to the authenticator that was invoked. An authenticator cannot claim an actor stored under another authenticator. Authenticator addresses SHOULD be deployed deterministically across chains. Dependent standards MAY define canonical authenticators and native implementations, provided native execution returns the same result as the contract. #### Native secp256k1 Authenticator `K1_AUTHENTICATOR = address(1)` represents the standard secp256k1 authenticator. Its data is the 65-byte `r || s || v` signature, and its result is: ```text actorId = bytes32(uint256(uint160(ecrecover(hash, v, r, s)))) ``` A zero recovered address is invalid. #### Delegate Authenticator A delegate authenticator allows account B to authenticate as an actor of account A. Account A registers: ```text actorId = bytes32(uint256(uint160(B))) authenticator = DELEGATE_AUTHENTICATOR ``` The delegate authenticator validates the nested authentication against account B and returns B's address-derived actor ID. Delegation MUST NOT chain. The nested authenticator MUST belong to a canonical set defined by the consuming standard, keeping validation work bounded. ### Account State The Keystore stores: ```text account_state[account] = multichain_sequence local_epoch local_sequence flags inline_self_actor ``` Each non-self actor occupies one `actor_config(account, actorId)` slot. The address-derived self actor MAY be stored inline in the account-state slot. The following flags are assigned: | Bit | Value | Name | Meaning | |---|---|---|---| | 0 | `0x01` | `CONTRACT_ESTABLISHED` | Authority was established through Keystore creation or import | | 1 | `0x02` | `DEFAULT_EOA_REVOKED` | The implicit address-bound secp256k1 actor is disabled | | 2–7 | | Reserved | Assigned only by dependent standards | Unknown flags have no effect unless assigned by a dependent standard. #### Implicit Address-Bound Actor Before Keystore establishment, an account MAY use the implicit actor: ```text actorId = bytes32(uint256(uint160(account))) authenticator = K1_AUTHENTICATOR scope = 0 expiry = 0 ``` This rule applies only when authentication used the native secp256k1 path and recovered the account's own address. A generic authenticator returning the same actor ID MUST NOT satisfy this rule. Once `DEFAULT_EOA_REVOKED` is set, the implicit actor is invalid. Revocation is permanent unless a dependent standard explicitly defines otherwise. #### Account Establishment Both creation and import set `CONTRACT_ESTABLISHED`. The flag is permanent and does not itself authorize an actor. Keystore state can outlive account code, including code removed under [EIP-6780](./eip-6780.md). Consumers MUST NOT treat empty code or delegated code plus Keystore state as proof that an address-bound private key exists. They MUST inspect `CONTRACT_ESTABLISHED`. ### Configuration Changes A signed configuration batch contains: ```text SignedAccountChanges { channel sequence changes[] auth } AccountChange { change_type payload } ``` The base change types are: | Type | Name | Payload | |---|---|---| | `0` | `AuthorizeActor` | `abi.encode(bytes32 actorId, ActorConfig config)` | | `1` | `RevokeActor` | `abi.encode(bytes32 actorId)` | | `2` | `IncrementLocalEpoch` | Empty | Dependent standards MAY assign additional change types. An implementation that does not support an assigned extension MUST reject that change atomically. The entire ordered batch is atomic. A rejected change reverts every preceding change in the same batch. For a non-self actor, `RevokeActor` deletes `actor_config`. `AuthorizeActor` for the address-derived self actor with `K1_AUTHENTICATOR` writes its inline configuration and clears `DEFAULT_EOA_REVOKED`. Revoking the self actor clears the inline configuration, deletes any non-secp256k1 self configuration, and sets `DEFAULT_EOA_REVOKED`. #### Authorization The batch is authorized by one live admin actor. Its `auth` value is `authenticator || data`. The [EIP-712](./eip-712.md)-style signature digest binds: ```text account resolved_chain_id channel sequence keccak256(ordered changes) ``` The digest MUST use a typed domain distinct from transaction and message signatures. Anyone may relay a signed batch. Authorization derives from the admin signature rather than `msg.sender`. #### Multichain Channel The Multichain channel binds `resolved_chain_id = 0` and uses a monotonic `uint64` sequence. The supplied sequence MUST equal `multichain_sequence`. A successful batch increments the stored sequence. The same signed batch can therefore be submitted independently on multiple chains whose counters remain aligned. A chain-specific extension change SHOULD NOT consume the Multichain counter when the same account is used on chains that do not implement that extension. #### Local Channel The Local channel binds `resolved_chain_id = block.chainid`. Its sequence is: ```text local_epoch (high 32 bits) || local_sequence (low 32 bits) ``` For a sequenced batch, the epoch and sequence MUST match storage. Success increments `local_sequence`. `UNSEQUENCED = uint32.max` selects an unsequenced local batch. It consumes no counter and remains replayable until `local_epoch` changes. Ordering between unsequenced batches is undefined. `IncrementLocalEpoch` increments `local_epoch` and resets `local_sequence` to zero. It invalidates unlanded local signatures from the previous epoch without changing live actors or the Multichain channel. Creation and import initialize `local_sequence = 1`. An all-zero local word denotes an uninitialized account. #### Expired Authorizations For an unsequenced batch, an `AuthorizeActor` change with a non-zero expiry already in the past is silently skipped. This prevents replayable just-in-time grants from overwriting newer state after expiry. For a single-consume Local or Multichain batch, the same actor configuration is installed inert and the sequence is consumed. ### EVM Configuration Path The Keystore exposes: ```solidity function applySignedAccountChanges( address account, SignedAccountChanges calldata changes ) external; ``` This function MUST authenticate the admin, validate the channel and sequence, and apply the ordered batch atomically. A protocol integration MAY apply the identical batch natively. Native application MUST produce the same storage changes, events, reverts, and sequence updates as the EVM function. ### Account Creation `createAccount` deploys runtime code and installs initial actors atomically: ```solidity function createAccount( bytes32 userSalt, bytes calldata code, InitialActor[] calldata initialActors ) external returns (address account); ``` Each initial actor is: ```text [actorId, authenticator, scope] ``` Initial actors are non-expiring. The input MUST be sorted by `actorId` in strictly ascending order. Duplicate actor IDs, zero actor IDs, and zero authenticators are invalid. Creation sets `CONTRACT_ESTABLISHED`, initializes `local_sequence = 1`, and sets `DEFAULT_EOA_REVOKED` unless the initial actors include the address-derived self actor bound to `K1_AUTHENTICATOR`. #### Address Derivation For each initial actor: ```text leaf_i = keccak256(actorId_i || authenticator_i || scope_i) actors_commitment = keccak256(leaf_0 || leaf_1 || ... || leaf_n) effective_salt = keccak256(user_salt || actors_commitment) deployment_code = DEPLOYMENT_HEADER(len(code)) || code account = keccak256( 0xff || KEYSTORE_ADDRESS || effective_salt || keccak256(deployment_code) )[12:] ``` The complete two-byte scope participates in the address. A wallet seeking the same address on multiple chains MUST use the same Keystore address, salt, code, ordered actors, authenticators, and scopes. `computeAddress` MUST perform the same initial-actor validation as `createAccount`. `DEPLOYMENT_HEADER(n)` is the following 14-byte loader: ```text 0x61, n[1], n[0], 0x60, 0x0e, 0x60, 0x00, 0x39, 0x61, n[1], n[0], 0x60, 0x00, 0xf3 ``` It copies the trailing `n` runtime-code bytes into memory and returns them. Callers supply runtime code only. ### Account Import `importAccount` registers actors for an existing deployed account: ```solidity function importAccount( address account, uint256 chainId, InitialActor[] calldata initialActors, bytes calldata signature ) external; ``` Import requires: 1. Deployed bytecode at `account`. 2. Uninitialized Local and Multichain sequences. 3. `chainId == 0 || chainId == block.chainid`. 4. A valid [ERC-1271](./eip-1271.md) signature from `account` over the typed initialization digest. 5. A valid, strictly sorted initial-actor set. Imported actors are non-expiring. Import sets `CONTRACT_ESTABLISHED`, initializes `local_sequence = 1`, and sets `DEFAULT_EOA_REVOKED` unless the initial actors include the address-derived self actor bound to `K1_AUTHENTICATOR`. ### Signature Verification `authenticateActor(account, hash, auth)` accepts: ```text authenticator (20 bytes) || data ``` It invokes the selected authenticator, resolves the returned actor, validates the authenticator binding and expiry, and returns `(actorId, scope)`. Invalid authentication reverts. For application messages, `validateSignature` accepts: ```text signature_type (1 byte) || authenticator (20 bytes) || data ``` Signature types are: | Value | Name | Chain binding | |---|---|---| | `0x01` | Local | `block.chainid` | | `0x02` | Multichain | `0` | The signed digest is: ```text replaySafeHash(account, chainId, hash) = keccak256(SIGNED_MESSAGE_TYPEHASH, account, chainId, hash) ``` The Keystore returns `(actorId, scope)` so the caller can make its own authorization decision. A smart account MAY expose [ERC-1271](./eip-1271.md) `isValidSignature` on top of `validateSignature`. The account MUST explicitly define which scopes may sign application messages; this proposal does not grant that authority to every stored actor. ### Portability The same Keystore contract, authenticator contracts, and creation code SHOULD be deployed at deterministic addresses across chains. | Component | Any EVM chain | Native integration | |---|---|---| | Keystore | Ordinary contract | Protocol may read the same storage directly | | Authenticators | EVM contracts | Canonical authenticators may be implemented natively | | Configuration | `applySignedAccountChanges` | Same batch may be transported by a native transaction | | Account creation | CREATE2 factory | Protocol may place the same runtime code directly | | Execution | ERC-4337 or wallet-specific | Native transaction standard | The Keystore does not define transaction nonces, gas payment, call execution, or a transaction type. Those belong to consuming standards. ### Constants | Name | Value | Meaning | |---|---|---| | `KEYSTORE_ADDRESS` | Deterministic deployment address | Keystore contract | | `K1_AUTHENTICATOR` | `address(1)` | secp256k1 authenticator | | `CONTRACT_ESTABLISHED` | `0x01` | Keystore-established account | | `DEFAULT_EOA_REVOKED` | `0x02` | Implicit address-bound actor revoked | | `UNSEQUENCED` | `uint32.max` | Local unsequenced sentinel | ## Rationale ### Why Separate Storage From Transport? An account's authority changes less frequently than its execution transport. Keeping authority in a common Keystore lets the same account use ERC-4337, a native transaction, or another transport without changing its actors or address. ### Why Store Authenticator Addresses? Authenticator addresses make signature algorithms replaceable and independently standardizable. An account can rotate algorithms by authorizing a new actor instead of migrating the account. ### Why Two Configuration Channels? The Multichain channel synchronizes authority across chains with one signature. The Local channel permits chain-specific changes and invalidation without disturbing the shared counter. ### Why Commit Initial Actors to the Address? Committing the complete initial actor set prevents an observer from deploying the same code and salt with attacker-controlled authority. ## Backwards Compatibility This proposal introduces new contracts and does not change existing transactions, EOAs, smart accounts, or ERC-4337 infrastructure. Adoption is opt-in. An ERC-4337 wallet can use the Keystore entirely through EVM calls. A native transaction standard may integrate the same storage and functions without changing contract-visible results. ## Reference Implementation ```solidity interface IKeystore { struct ChangeSequences { uint64 multichain; uint32 localEpoch; uint32 localSequence; } struct ActorConfig { address authenticator; uint48 expiry; uint16 scope; } struct InitialActor { bytes32 actorId; address authenticator; uint16 scope; } enum AccountChangeChannel { Local, Multichain } enum ChangeType { AuthorizeActor, RevokeActor, IncrementLocalEpoch } struct AccountChange { ChangeType changeType; bytes payload; } struct SignedAccountChanges { AccountChangeChannel channel; uint64 sequence; AccountChange[] changes; bytes auth; } function createAccount( bytes32 userSalt, bytes calldata code, InitialActor[] calldata initialActors ) external returns (address); function computeAddress( bytes32 userSalt, bytes calldata code, InitialActor[] calldata initialActors ) external view returns (address); function importAccount( address account, uint256 chainId, InitialActor[] calldata initialActors, bytes calldata signature ) external; function applySignedAccountChanges( address account, SignedAccountChanges calldata changes ) external; function authenticateActor( address account, bytes32 hash, bytes calldata auth ) external view returns (bytes32 actorId, uint16 scope); function validateSignature( address account, bytes32 hash, bytes calldata auth ) external view returns (bytes32 actorId, uint16 scope); function replaySafeHash( address account, uint256 chainId, bytes32 hash ) external pure returns (bytes32); function getActorConfig( address account, bytes32 actorId ) external view returns (ActorConfig memory); function getChangeSequences( address account ) external view returns (ChangeSequences memory); function isContractEstablished( address account ) external view returns (bool); } ``` ## Security Considerations **Authenticator binding:** The Keystore must verify that the returned actor ID is stored under the authenticator that produced it. Otherwise, a malicious authenticator could impersonate another actor. **Implicit actor restriction:** Only native secp256k1 recovery of the account's own address may use the implicit actor. A generic authenticator returning the same actor ID must not gain implicit admin authority. **Actor expiry:** Wallets should retain at least one non-expiring admin. Expiring the only admin can make future configuration impossible. **Configuration replay:** Multichain and sequenced Local batches are single-consume. Unsequenced Local batches remain replayable until the local epoch changes. **Cross-chain divergence:** Keystore state is chain-local even when signatures are multichain. Wallets must relay batches to every intended chain and preserve counter alignment. Extension-only changes should use the Local channel when unsupported chains share the same Multichain counter. **Account creation:** Initial actor commitments prevent authority front-running. Wallet runtime code should remain inert until initialized because deterministic code deployment may be permissionless. **Contract establishment:** Keystore state can survive code removal. Empty code does not prove that an account is controlled by an address-bound private key. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).