--- eip: 8357 title: EVM Verification Key Registry description: Exposes fork-approved EVM verification key hashes and their stateless input schema IDs to contracts author: Luca Donno (@lucadonnoh) discussions-to: https://ethereum-magicians.org/t/eip-8357-evm-verification-key-registry/29222 status: Draft type: Standards Track category: Core created: 2026-07-30 requires: 161, 1559, 7910, 7928, 7997, 8025, 8037, 8288 --- ## Abstract This EIP creates a fixed-address system contract containing the canonical EVM verification key hash for each registered L1 feature fork. Each entry maps one exact verification key hash to the `schema_id` of the stateless input accepted by the fork-specific EVM program bound by the corresponding verification key. The contract stores one `current_verification_key_hash`. A caller may retrieve either the current entry or an exact historical entry. This lets a native rollup follow L1 EVM upgrades automatically or deliberately remain on a historical EVM fork. ## Motivation Native rollups are intended to inherit Ethereum's execution environment and upgrade process instead of maintaining a bespoke verifier and its governance. The rollup contract must therefore identify the verification key that L1 has approved for proving EVM execution. [EIP-8288](./eip-8288.md) verifies proofs against explicitly supplied verification key hashes, but does not tell contracts which hashes L1 recognizes, which one is current, or which input schema each program accepts. A contract needs that schema to reconstruct the program's public output, which commits to it. Hard-coding those values would force each rollup to upgrade through its own governance at every L1 feature fork or remain on an older EVM. ## 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](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). ### Parameters | Constant | Value | |---|---:| | `SYSTEM_ADDRESS` | `0xfffffffffffffffffffffffffffffffffffffffe` | | `EVM_VK_REGISTRY_ADDRESS` | `0x00005e9c1447c1a05a642ec9eb76d9c125468357` | | `REGISTRY_DEPLOYMENT_SALT` | `0x5c4fde244aecad9b7c039f836e4f6bff9978d838dd6d0ae179f3c70aa84a997f` | | `CURRENT_VERIFICATION_KEY_HASH_SLOT` | `0` | | `SCHEMA_ID_MAPPING_SLOT` | `1` | The all-zero verification key hash is reserved for current-entry lookup and MUST NOT be registered. ### Registry state `verification_key_hash` is the 32-byte field defined by EIP-8288 for `LEANSTARK_SCHEME`: a hash of a STARK verification key. A registered `verification_key_hash` identifies the L1-approved verification key for a deterministic, fork-specific EVM stateless-validation program. Each registered `verification_key_hash` stores the nonzero `uint16` `schema_id` of the stateless input that its program accepts, as defined by [EIP-8025](./eip-8025.md). The `schema_id` identifies the input schema and the fork rules that the program executes, and is echoed in the program's public output. Storage slot `CURRENT_VERIFICATION_KEY_HASH_SLOT` stores the current `verification_key_hash`. Registering a new hash or reactivating a previously registered hash replaces this value. Previously registered entries remain available through exact-hash lookup. Entries are append-only. A registered entry MUST NOT be deleted or overwritten. For each nonzero `verification_key_hash`, define its schema ID slot using the standard Solidity mapping layout: ```python def schema_id_slot(verification_key_hash: Bytes32) -> U256: return U256( keccak256( verification_key_hash + uint256_be(SCHEMA_ID_MAPPING_SLOT) ) ) ``` A `verification_key_hash` is registered if `sload(schema_id_slot(verification_key_hash))` is nonzero. The stored value MUST be at most `2**16 - 1`. ### Contract interface The contract selects its operation by `CALLER`: - if `CALLER == SYSTEM_ADDRESS`, execute the fork-update operation; - otherwise, execute the read operation. All calls MUST have value `0`. In calldata and return data, `schema_id` MUST be encoded as a zero-padded, 32-byte big-endian word. #### Read A non-system caller MUST provide exactly 32 bytes of calldata: - an all-zero word requests the current entry; or - a nonzero word requests that exact `verification_key_hash`. For a valid request, the contract MUST return exactly 64 bytes: ```text verification_key_hash || schema_id ``` For an all-zero query, `verification_key_hash` MUST be loaded from `CURRENT_VERIFICATION_KEY_HASH_SLOT`. For a nonzero query, `verification_key_hash` MUST equal the supplied value. The call MUST revert without return data if: - calldata is malformed; - call value is nonzero; - no registered entry exists for the selected `verification_key_hash`. The read operation MUST NOT modify state. #### Fork update The system caller supplies either a 64-byte registration or a 32-byte reactivation. For a registration, calldata is: ```text verification_key_hash || schema_id ``` The call registers a new verification key hash and updates the current pointer to it. The first fork update registers only the hash active at the registry's activation. - `verification_key_hash` MUST be nonzero; - `verification_key_hash` MUST NOT already be registered; - `schema_id` MUST be nonzero and at most `2**16 - 1`. For a reactivation, calldata is: ```text verification_key_hash ``` The call updates the current pointer to a previously registered verification key hash without modifying its `schema_id`. - `verification_key_hash` MUST be nonzero; - `verification_key_hash` MUST already be registered. If any condition fails, the call MUST revert. On success, `CURRENT_VERIFICATION_KEY_HASH_SLOT` is set to `verification_key_hash` and no data is returned. A successful registration also stores the supplied `schema_id`. The fork-update operation is: ```python def update_registry(calldata: Bytes) -> None: assert caller == SYSTEM_ADDRESS assert callvalue == 0 if len(calldata) == 64: verification_key_hash = calldata[0:32] schema_id = uint256_be_decode(calldata[32:64]) assert verification_key_hash != bytes32(0) assert 0 < schema_id <= 2**16 - 1 slot = schema_id_slot(verification_key_hash) assert sload(slot) == 0 sstore(slot, schema_id) elif len(calldata) == 32: verification_key_hash = calldata[0:32] assert verification_key_hash != bytes32(0) assert sload(schema_id_slot(verification_key_hash)) != 0 else: assert False sstore(CURRENT_VERIFICATION_KEY_HASH_SLOT, verification_key_hash) ``` ### Adding a verification key hash Each new `verification_key_hash` MUST be specified by a distinct Core EIP that lists this EIP in its `requires` header. The hash is activated in a hard fork. That EIP MUST define: 1. the exact `verification_key_hash`; 2. the L1 feature fork whose EVM semantics the corresponding verification key represents; and 3. the `schema_id` of the stateless input that the corresponding program accepts. The hard fork that activates this EIP MUST also activate exactly one such Core EIP. Its hash is the registry's initial `verification_key_hash`. ### Reactivating a verification key hash A hard fork MAY reactivate a previously registered `verification_key_hash`. A distinct Core EIP that lists this EIP in its `requires` header MUST define: 1. the exact previously registered `verification_key_hash` to reactivate; 2. the L1 feature fork whose EVM semantics the corresponding verification key represents. Reactivation changes only `CURRENT_VERIFICATION_KEY_HASH_SLOT`. It MUST NOT change the hash's stored `schema_id`. ### Block processing `EVM_VK_REGISTRY_UPDATE` is the registration or reactivation calldata defined above. For a registration, `schema_id` is the one defined by the Core EIP that specifies the hash. In the first block where a hard fork selects a new or reactivated `verification_key_hash` as current, including the registry activation fork, before processing transactions, execution clients MUST call `EVM_VK_REGISTRY_ADDRESS` as `SYSTEM_ADDRESS` with `EVM_VK_REGISTRY_UPDATE` as calldata and value `0`. This is a system operation and therefore: - the call MUST have the dedicated `SYSTEM_CALL_GAS_LIMIT` defined by [EIP-8037](./eip-8037.md); - gas consumed by the call MUST NOT contribute to either `block_execution_gas_used` or `block_state_gas_used`; - both the gas limit assigned to the call and the gas consumed MUST be excluded from checks against the block's gas limit; - the call MUST NOT follow [EIP-1559](./eip-1559.md) fee-burning semantics; - the call MUST be treated as a pre-execution system-contract call under [EIP-7928](./eip-7928.md); and - the block MUST be invalid if `EVM_VK_REGISTRY_ADDRESS` contains no code or the call fails. The call MUST NOT be performed in any other block. ### Bytecode The registry runtime bytecode, `REGISTRY_RUNTIME_CODE`, is: ```text 0x3460a1573373fffffffffffffffffffffffffffffffffffffffe14604a576020360360a1575f3580602c57545b801560a1575f52600160205260405f2054801560a15760205260405ff35b602036146085576040360360a1575f35801560a157805f52600160205260405f20805460a157602035801560a1578060101c60a15790555f55005b5f35801560a157805f52600160205260405f20541560a1575f55005b5f5ffd ``` The registry initcode, `REGISTRY_INITCODE`, is: ```text 0x60a58060095f395ff33460a1573373fffffffffffffffffffffffffffffffffffffffe14604a576020360360a1575f3580602c57545b801560a1575f52600160205260405f2054801560a15760205260405ff35b602036146085576040360360a1575f35801560a157805f52600160205260405f20805460a157602035801560a1578060101c60a15790555f55005b5f35801560a157805f52600160205260405f20541560a1575f55005b5f5ffd ``` The initcode consists of a simple constructor followed by `REGISTRY_RUNTIME_CODE`. The constructor returns the runtime bytecode without initializing storage. ### Deployment The registry MUST be deployed through the `FACTORY_ADDRESS` defined by [EIP-7997](./eip-7997.md). Any account MAY deploy it with an ordinary transaction that calls the factory with the 32-byte deployment salt followed by the initcode: ```text REGISTRY_DEPLOYMENT_SALT || REGISTRY_INITCODE ``` Under [EIP-1014](./eip-1014.md), the registry address is: ```text EVM_VK_REGISTRY_ADDRESS = keccak256( 0xff || FACTORY_ADDRESS || REGISTRY_DEPLOYMENT_SALT || keccak256(REGISTRY_INITCODE) )[12:] ``` Deploying the contract does not activate this EIP or register a `verification_key_hash`. The deployment transaction MUST be included in a block preceding the first block of the registry activation fork because the initial registry system call is performed before transactions in the activation block. From the registry activation fork onward: - the account at `EVM_VK_REGISTRY_ADDRESS` MUST contain `REGISTRY_RUNTIME_CODE` and have nonce `1` under [EIP-161](./eip-161.md); and - every applicable [EIP-7910](./eip-7910.md) `systemContracts` object MUST include an `EVM_VK_REGISTRY_ADDRESS` member with the registry address, preserving alphabetical key order. ## Rationale ### Deterministic deployment [EIP-7997](./eip-7997.md) allows any account to deploy the contract. Because `EVM_VK_REGISTRY_ADDRESS` is bound to `REGISTRY_INITCODE`, changing the runtime requires different initcode and therefore produces a different address. ### Current and pinned verification key hashes A native rollup can query the current entry to follow L1 EVM upgrades without a separate governance action, or pin a historical entry while its infrastructure adapts. The stored `schema_id` lets either mode reconstruct the program's public output, which commits to the input schema and fork rules the program executed. ### Registry instead of a verifier wildcard A verifier wildcard could select the current verification key, but would duplicate that policy in every proof-verification mechanism. The registry records current and historical verification key hashes once, while verifiers continue accepting exact hashes. Contracts can follow the current pointer or pin any registered hash. The all-zero value is only a registry lookup selector, never a verification key hash. ### Reactivating a historical verification key hash A later hard fork can restore a previously registered verification key hash as current without inventing a different verification key for the same verification relation. The hash retains its `schema_id` because the schema is a property of the program that the verification key identifies, not of the hard fork that reactivates it. Caller-based operation selection makes the 32-byte system reactivation unambiguous from a 32-byte read: only `SYSTEM_ADDRESS` executes the fork-update operation, while every other caller executes the read operation. ## Backwards Compatibility This EIP introduces backward-incompatible changes to the block validation rules and must be activated through a hard fork. Existing transaction formats are unchanged. ## Test Cases Let: ```text K1 = 0x0000000000000000000000000000000000000000000000000000000000000001 K2 = 0x0000000000000000000000000000000000000000000000000000000000000002 I1 = 0x0000000000000000000000000000000000000000000000000000000000001501 I2 = 0x000000000000000000000000000000000000000000000000000000000000ffff S1 = 0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f S2 = 0xd9d16d34ffb15ba3a3d852f0d403e2ce1d691fb54de27ac87cd2f993f3ec330f ``` `S1` and `S2` are `schema_id_slot(K1)` and `schema_id_slot(K2)`, respectively. Starting from a registry deployed with `REGISTRY_INITCODE` and empty storage: 1. A zero-value, non-system call with calldata `bytes32(0)` reverts with no return data. 2. A zero-value system call with calldata `K1 || I1` succeeds with no return data. Storage slot `0` becomes `K1` and storage slot `S1` becomes `I1`. 3. Zero-value, non-system calls with calldata `bytes32(0)` and `K1` each return `K1 || I1`. 4. A zero-value system call with calldata `K1 || I2` reverts with no return data and leaves both storage slots unchanged. 5. A zero-value system call with calldata `K2 || I2` succeeds with no return data. Storage slot `0` becomes `K2`, storage slot `S2` becomes `I2`, and storage slot `S1` remains `I1`. 6. A zero-value system call with calldata `K1` succeeds with no return data. Storage slot `0` becomes `K1`, while `S1` and `S2` remain unchanged. 7. A zero-value, non-system call with calldata `bytes32(0)` returns `K1 || I1`, and one with calldata `K2` returns `K2 || I2`. Calls with nonzero value revert with no return data. Non-system calldata lengths other than 32 bytes and system calldata lengths other than 32 or 64 bytes also revert with no return data. Registrations with a zero verification key hash, zero schema ID, or schema ID greater than `2**16 - 1`, and attempts to reactivate a zero or unregistered hash, revert without changing state. ## Reference Implementation See [`src/verification_key_registry`](https://github.com/ethereum/sys-asm/tree/6be87c5ae1cb2226efa43a8479dfa502b278835b/src/verification_key_registry). ## Security Considerations Consumers that select the current entry automatically adopt each fork-selected verification key hash. Proofs generated under the previous verification key may fail after the switch. A rollup unable to produce proofs under the new verification key may halt. Consumers requiring an independent upgrade window can pin an exact registered hash. Registered verification key hashes are never revoked, even if proofs under the corresponding verification keys are later considered insecure. Consumers selecting a historical entry assume this risk. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).