---
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, 1014, 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).
