EIP.tools

EIP.tools

⚠️ DraftStandards Track: Core

EIP-9999: Post-quantum-ready deposit contract

Adds a variable-length validator deposit contract with an irreversible BLS retirement mode.

Authors
Created2026-08-23
Requires

Markdown

https://raw.githubusercontent.com/ethereum/EIPs/re...
Pull Request#12235PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationConstantsDefinitionsDeploymentContract interfaceDeposit operationDeposit eventMode-transition system callsExecution-layer request derivationConsensus-layer containersConsensus-layer processingBLS retirementRationaleExplicit scheme identifierContract-side BLS length enforcementCredential length capsLog-derived requestsSeparate request fieldOne-way mode transitionsBackwards CompatibilityContract and request phasesTest CasesReference ImplementationSecurity ConsiderationsPermanently locked depositsLegacy contract after retirementIncorrect scheme valueSystem-call safetyCredential identityCopyright

Abstract

This EIP adds a forward-compatible validator deposit contract with variable-length public keys and credential metadata. Each deposit declares a credential scheme where scheme 0 represents BLS deposits. The execution layer carries accepted deposits to consensus through EIP-7685.

This EIP does not activate a new validator credential scheme. A future fork can assign new schemes and permanently retire BLS deposits through a system call.

Motivation

The current validator deposit contract fixes the public key and signature to the BLS12-381 sizes. These sizes prevent the contract from carrying larger or otherwise different post-quantum credentials.

The existing contract also maintains a Merkle tree for the original deposit polling mechanism. EIP-6110 replaced this mechanism with requests derived from deposit logs. The new contract can therefore omit the tree and deposit_data_root.

Like the existing path, the new contract transports deposit data but leaves cryptographic validation to consensus. Its scheme identifier selects the consensus rules for the opaque credential fields.

Concurrent BLS support lets tooling migrate before a later fork retires BLS onboarding.

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.

Constants

NameValueDescription
CREDENTIAL_DEPOSIT_CONTRACT_ADDRESSTBDAddress of the new deposit contract
CREDENTIAL_DEPOSIT_CONTRACT_SALTTBDSalt for the deterministic deployment
CREDENTIAL_DEPOSIT_CONTRACT_INIT_CODETBDInitialization code for the deterministic deployment
CREDENTIAL_DEPOSIT_CONTRACT_RUNTIME_CODETBDRequired runtime code
CREDENTIAL_DEPOSIT_REQUEST_TYPETBDEIP-7685 request-type byte
CREDENTIAL_DEPOSIT_EVENT_SIGNATURE_HASH0x573a3e66153a38882575e538b0f3fc14cc9ea961a3c7d3b647fb345fab5ea1d6Event topic defined below
INITIAL_ACTIVATION_TIMESTAMPTBDTimestamp that enables the new deposit contract and request type
BLS_RETIREMENT_TIMESTAMPTBDTimestamp that retires BLS deposits
SYSTEM_ADDRESS0xfffffffffffffffffffffffffffffffffffffffeCaller for protocol system calls
SYSTEM_CALL_GAS_LIMIT30_000_000Dedicated gas for a system call
DEPOSIT_MODE_STORAGE_SLOT0Storage slot for the contract mode
DEPOSIT_MODE_DISABLED0Deposits are disabled
DEPOSIT_MODE_BLS_ENABLED1BLS deposits are accepted
DEPOSIT_MODE_BLS_RETIRED2BLS deposits are rejected
ENABLE_DEPOSITS_COMMAND0x00System command for initial activation
RETIRE_BLS_COMMAND0x01System command for BLS retirement
MIN_DEPOSIT_AMOUNT1_000_000_000Minimum deposit amount in gwei
MAX_PUBKEY_LENGTH8192Maximum pubkey length in bytes
MAX_CREDENTIAL_METADATA_LENGTH8192Maximum credential_metadata length in bytes
BLS_CREDENTIAL_SCHEME0Scheme identifier for BLS deposits
BLS_PUBKEY_LENGTH48Length of a BLS12-381 public key
BLS_CREDENTIAL_METADATA_LENGTH96Length of BLS credential metadata

CREDENTIAL_DEPOSIT_REQUEST_TYPE MUST be globally unique among assigned EIP-7685 request types.

BLS_RETIREMENT_TIMESTAMP MUST be greater than or equal to INITIAL_ACTIVATION_TIMESTAMP.

The event-signature hash is:

keccak256("CredentialDepositEvent(uint8,bytes,bytes32,uint64,bytes)")

Definitions

The initial activation block is the first execution block with:

block.timestamp >= INITIAL_ACTIVATION_TIMESTAMP

The BLS retirement block is the first execution block with:

block.timestamp >= BLS_RETIREMENT_TIMESTAMP

A deposit or request is BLS if and only if:

scheme == BLS_CREDENTIAL_SCHEME

This EIP assigns only the BLS scheme value. All other scheme values are unassigned. A future credential-scheme EIP MUST assign an unused scheme value and MUST define its validation.

Deployment

The EIP-7997 factory MUST deploy the contract before initial activation. Its call input MUST be:

CREDENTIAL_DEPOSIT_CONTRACT_SALT || CREDENTIAL_DEPOSIT_CONTRACT_INIT_CODE

The resulting address MUST equal CREDENTIAL_DEPOSIT_CONTRACT_ADDRESS. The runtime code MUST equal CREDENTIAL_DEPOSIT_CONTRACT_RUNTIME_CODE.

The deployment MUST initialize DEPOSIT_MODE_STORAGE_SLOT to DEPOSIT_MODE_DISABLED. A deposit call MUST revert while this mode is active.

At and after initial activation, a block is invalid if the contract is absent or has different runtime code.

This EIP MUST activate after the EIP-6110 transition is complete. EIP-6110 defines this point as:

state.eth1_deposit_index == state.deposit_requests_start_index

Contract interface

The contract MUST expose this user function:

function deposit(
    uint8 scheme,
    bytes calldata pubkey,
    bytes32 withdrawal_credentials,
    bytes calldata credential_metadata
) external payable;

Deposit operation

The contract MUST reject a deposit unless all these amount conditions are true:

  1. msg.value is a multiple of 1 gwei.
  2. msg.value / 1 gwei is at least MIN_DEPOSIT_AMOUNT.
  3. msg.value / 1 gwei is at most 2**64 - 1.

The contract MUST reject a deposit if either condition is true:

  1. len(pubkey) > MAX_PUBKEY_LENGTH.
  2. len(credential_metadata) > MAX_CREDENTIAL_METADATA_LENGTH.

These limits apply in every mode and to every scheme.

Except in DEPOSIT_MODE_DISABLED, the contract MUST accept an unassigned scheme that satisfies the amount and length limits above. It MUST NOT validate credential contents or perform cryptographic validation.

In DEPOSIT_MODE_DISABLED, every deposit MUST revert.

In DEPOSIT_MODE_BLS_ENABLED, a BLS deposit MUST revert unless both conditions are true:

  1. len(pubkey) == BLS_PUBKEY_LENGTH.
  2. len(credential_metadata) == BLS_CREDENTIAL_METADATA_LENGTH.

A BLS deposit that satisfies both conditions and the amount limits MUST succeed.

A deposit with any other scheme that satisfies the amount and length limits MUST succeed.

In DEPOSIT_MODE_BLS_RETIRED, a BLS deposit MUST revert.

Deposit event

A successful deposit MUST emit exactly one event:

event CredentialDepositEvent(
    uint8 scheme,
    bytes pubkey,
    bytes32 withdrawal_credentials,
    uint64 amount,
    bytes credential_metadata
);

amount is uint64(msg.value / 1 gwei).

The event has exactly one topic:

topics[0] = CREDENTIAL_DEPOSIT_EVENT_SIGNATURE_HASH

The event data is the canonical Solidity ABI encoding of:

(scheme, pubkey, withdrawal_credentials, amount, credential_metadata)

The event contains no indexed parameters.

Mode-transition system calls

At the start of the initial activation block, the execution layer MUST call CREDENTIAL_DEPOSIT_CONTRACT_ADDRESS with:

caller:   SYSTEM_ADDRESS
calldata: ENABLE_DEPOSITS_COMMAND
value:    0
gas:      SYSTEM_CALL_GAS_LIMIT

The contract MUST require that the current mode is DEPOSIT_MODE_DISABLED. The call MUST set the mode to DEPOSIT_MODE_BLS_ENABLED.

At the start of the BLS retirement block, the execution layer MUST call the same address with:

caller:   SYSTEM_ADDRESS
calldata: RETIRE_BLS_COMMAND
value:    0
gas:      SYSTEM_CALL_GAS_LIMIT

The contract MUST require that the current mode is DEPOSIT_MODE_BLS_ENABLED. The call MUST set the mode to DEPOSIT_MODE_BLS_RETIRED.

The contract MUST reject all other system commands and all repeated or reverse transitions.

Execution clients MUST use this block-start order:

  1. Perform the EIP-4788 beacon-root call, if active.
  2. Perform the EIP-2935 block-hash call, if active.
  3. Perform the enable call in the initial activation block.
  4. Perform the retirement call in the BLS retirement block.
  5. Process transactions.

If both timestamps activate in one block, the execution client MUST perform the enable call before the retirement call.

Each system call MUST return no data and emit no log. The call uses dedicated gas that does not count against the block gas limit.

If a required system call fails, the block is invalid. Deposits in each activation block observe the new mode because the call occurs before transactions.

Execution-layer request derivation

Execution clients MUST inspect receipts in transaction order. They MUST inspect logs in their order within each receipt.

Clients MUST decode every log from CREDENTIAL_DEPOSIT_CONTRACT_ADDRESS with decode_credential_deposit_event. A decode failure makes the block invalid. The execution layer MUST NOT classify, filter, or discard a decoded request based on its scheme, credential lengths, or contents.

The request types use ProgressiveContainer from EIP-7495. They use ProgressiveList and ProgressiveByteList from EIP-7916. EIP-7688 defines their use in evolving consensus containers.

class CredentialDepositRequest(
    ProgressiveContainer(active_fields=[1, 1, 1, 1, 1])
):
    scheme: uint8
    pubkey: ProgressiveByteList
    withdrawal_credentials: Bytes32
    amount: Gwei
    credential_metadata: ProgressiveByteList


class CredentialDepositRequests(
    ProgressiveList[CredentialDepositRequest]
):
    pass

The contract enforces the field-length caps. The progressive SSZ schema does not repeat them.

Clients construct one request list from both deposit contracts. The list preserves transaction, receipt, and log order across both addresses.

EVENT_TYPES = ("uint8", "bytes", "bytes32", "uint64", "bytes")


def decode_credential_deposit_event(log):
    assert log.topics == [CREDENTIAL_DEPOSIT_EVENT_SIGNATURE_HASH]
    decoded = abi_decode(EVENT_TYPES, log.data)
    assert abi_encode(EVENT_TYPES, decoded) == log.data
    return decoded


def legacy_log_to_credential_deposit_request(log):
    if len(log.topics) == 0 or log.topics[0] != DEPOSIT_EVENT_SIGNATURE_HASH:
        return None

    assert is_valid_deposit_event_data(log.data)
    pubkey, withdrawal_credentials, amount, signature, _index = (
        parse_deposit_data(log.data)
    )
    return CredentialDepositRequest(
        scheme=BLS_CREDENTIAL_SCHEME,
        pubkey=ProgressiveByteList(pubkey),
        withdrawal_credentials=Bytes32(withdrawal_credentials),
        amount=Gwei(int.from_bytes(amount, "little")),
        credential_metadata=ProgressiveByteList(signature),
    )


def get_credential_deposit_requests(block) -> CredentialDepositRequests:
    requests = CredentialDepositRequests()

    for receipt in block.receipts:
        for log in receipt.logs:
            if log.address == CREDENTIAL_DEPOSIT_CONTRACT_ADDRESS:
                (
                    scheme,
                    pubkey,
                    withdrawal_credentials,
                    amount,
                    credential_metadata,
                ) = decode_credential_deposit_event(log)

                requests.append(
                    CredentialDepositRequest(
                        scheme=uint8(scheme),
                        pubkey=ProgressiveByteList(pubkey),
                        withdrawal_credentials=Bytes32(withdrawal_credentials),
                        amount=Gwei(amount),
                        credential_metadata=ProgressiveByteList(
                            credential_metadata
                        ),
                    )
                )
                continue

            if (
                log.address != DEPOSIT_CONTRACT_ADDRESS
                or block.timestamp >= BLS_RETIREMENT_TIMESTAMP
            ):
                continue

            request = legacy_log_to_credential_deposit_request(log)
            if request is not None:
                requests.append(request)

    return requests

abi_decode and abi_encode use the canonical Solidity ABI. Decoding MUST reject an invalid type encoding. Re-encoding MUST reproduce the complete event data, including offsets and zero padding.

DEPOSIT_CONTRACT_ADDRESS, DEPOSIT_EVENT_SIGNATURE_HASH, is_valid_deposit_event_data, and parse_deposit_data are defined by EIP-6110. The legacy deposit index is not copied into the generalized request. This EIP activates after the EIP-6110 transition, so consensus no longer needs that index.

The EIP-7685 request object is:

request_type = CREDENTIAL_DEPOSIT_REQUEST_TYPE
request_data = ssz_serialize(get_credential_deposit_requests(block))
request = request_type + request_data

If the request list is empty, the execution layer MUST omit this request type. Otherwise, it MUST include exactly one request object for this type.

The existing Engine API executionRequests sequence carries the request. This EIP adds no Engine API field. The execution client MUST verify that request_data equals the SSZ serialization derived from the block receipts.

Beginning with the initial activation block, execution clients MUST stop deriving EIP-6110 request objects. A valid EIP-7685 request list MUST NOT contain a DEPOSIT_REQUEST_TYPE object from EIP-6110.

Consensus-layer containers

The fork appends the following field to ExecutionRequests:

class ExecutionRequests(ProgressiveContainer):
    # Existing fields are omitted.
    credential_deposits: CredentialDepositRequests

Consensus-layer processing

The deposits field MUST remain in ExecutionRequests. It MUST be empty in the initial activation block and every later block.

At initial activation, the non-BLS handler is:

def process_non_bls_credential_deposit(
    state: BeaconState,
    request: CredentialDepositRequest,
) -> None:
    return

The BLS handler is:

def process_bls_credential_deposit(
    state: BeaconState,
    request: CredentialDepositRequest,
) -> None:
    state.pending_deposits.append(
        PendingDeposit(
            pubkey=BLSPubkey(request.pubkey),
            withdrawal_credentials=request.withdrawal_credentials,
            amount=request.amount,
            signature=BLSSignature(request.credential_metadata),
            slot=state.slot,
        )
    )


def process_credential_deposit_request(
    state: BeaconState,
    request: CredentialDepositRequest,
) -> None:
    if request.scheme == BLS_CREDENTIAL_SCHEME:
        process_bls_credential_deposit(state, request)
        return

    process_non_bls_credential_deposit(state, request)

A valid execution payload cannot contain a BLS request with other field lengths. The pinned runtime code rejects such a deposit from the new contract. The EIP-6110 decoder rejects a legacy deposit event with other field lengths.

PendingDeposit and its processing are defined by EIP-7251. Thus, BLS signature validation, deposit churn, and validator activation remain unchanged.

The operation order is:

# Existing operations before execution requests are omitted.
assert len(body.execution_requests.deposits) == 0
for_ops(
    body.execution_requests.credential_deposits,
    process_credential_deposit_request,
)
for_ops(body.execution_requests.withdrawals, process_withdrawal_request)
for_ops(body.execution_requests.consolidations, process_consolidation_request)

process_non_bls_credential_deposit MUST leave the state unchanged. The block retains the request, but BeaconState does not. A credential-scheme EIP can replace this function. It MUST define validation, state representation, and top-up behavior for its assigned scheme.

BLS retirement

Before BLS retirement, execution clients MUST convert BLS deposits from both contracts into CredentialDepositRequest objects. Consensus MUST process them through the same handler.

Beginning with the BLS retirement block, execution clients MUST stop converting legacy deposit-contract events into generalized requests. They MUST continue to derive every request from the new contract without inspecting its scheme or credential format.

The consensus-layer request processors do not require a BLS retirement branch. A valid execution payload cannot contain a new BLS request after retirement.

Pending deposits that entered state.pending_deposits before BLS retirement remain eligible for normal pending-deposit processing after retirement.

Rationale

Explicit scheme identifier

Each deposit declares its credential scheme and carries scheme-specific metadata. BLS uses a signature. Consensus interprets both fields, so the contract does not need a scheme registry.

Contract-side BLS length enforcement

BLS length checks preserve the existing interface constraint. A wrong-length deposit reverts before the contract locks its ETH. Other invalid BLS deposits retain the legacy path's lock behavior.

Credential length caps

Execution gas prices calldata and log data, but not consensus propagation and processing. The field caps bound each request. The block gas limit bounds the aggregate request data.

The 8192-byte caps leave room for larger credentials and keep the first variable-length request type bounded.

Log-derived requests

The contract needs no queue because each deposit pays execution gas and locks at least 1 ETH. Receipt order gives a canonical request order.

Separate request field

Repurposing the existing deposits field changes its element type. The field therefore remains empty after initial activation.

One-way mode transitions

The mode only advances. The disabled mode exists because a pre-activation deposit emits an event that clients do not convert, so no validator receives the locked ETH. The retired mode is irreversible, so no later system call can re-enable quantum-vulnerable BLS onboarding.

Backwards Compatibility

This EIP adds a contract, an EIP-7685 request type, and a field in ExecutionRequests. These changes require a coordinated execution-layer and consensus-layer fork.

At initial activation, execution clients stop deriving EIP-6110 requests and use the generalized request type for both deposit contracts.

The new ABI is not drop-in compatible. It adds scheme, removes deposit_data_root, and changes withdrawal_credentials from dynamic bytes to bytes32. The legacy contract remains the compatible path for unmodified BLS tooling until retirement.

Contract and request phases

The deployment window starts after the EIP-6110 transition and contract deployment. It ends before the initial activation block. The migration period ends before the BLS retirement block.

PhaseNew contractLegacy contractExecution-layer derivationConsensus-layer processing
Deployment windowRejects all depositsAccepts valid legacy depositsDerives only EIP-6110 requestsProcesses only legacy deposits
Migration periodAccepts deposits under BLS_ENABLEDAccepts valid legacy depositsConverts both event formats into the generalized request typeProcesses one ordered generalized list
Post-retirementRejects BLS and accepts valid non-BLS depositsStill accepts valid legacy depositsDerives generalized requests only from the new contractProcesses the generalized list

Test Cases

Implementations MUST pass the test cases.

Reference Implementation

The Solidity reference implementation illustrates the required contract behavior. The final bytecode and deployment constants remain TBD.

Security Considerations

Permanently locked deposits

A wrong-length BLS deposit reverts. An invalid exact-length deposit or an unassigned scheme can still pass contract checks. Consensus does not retain or replay an ignored request, and the contract retains its ETH.

Deposit tooling must use an assigned scheme value, match the credentials to that scheme, and validate all available proofs before it sends a transaction.

Legacy contract after retirement

The immutable legacy contract accepts deposits included in or after the retirement block, but the execution layer creates no request. This applies even if the user signed and broadcast the transaction before retirement. The inclusion block controls the result.

Wallets, launchpads, staking providers, and explorers must stop legacy deposits well before retirement. They must make sure that no legacy deposit transaction remains pending at the boundary.

Incorrect scheme value

The contract applies BLS rules only when scheme is 0. Consensus processes each request under its declared scheme and never infers BLS from field lengths. An incorrect scheme cannot bypass BLS retirement and can permanently lock the deposited ETH.

System-call safety

Only SYSTEM_ADDRESS can change the mode. Each transition requires the correct mode and one-byte command. A failed required call makes the block invalid.

Credential identity

This EIP does not define how a future variable-length public key maps to a validator identity. A future credential-scheme EIP must define registry uniqueness, duplicate deposits, top-ups, and key replacement.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth