--- eip: 9999 title: Post-quantum-ready deposit contract description: Adds a variable-length validator deposit contract with an irreversible BLS retirement mode. author: Kevaundray Wedderburn (@kevaundray), Tom Wambsgans (@tomWambsgans), Thomas Coratger (@tcoratger) discussions-to: status: Draft type: Standards Track category: Core created: 2026-08-23 requires: 6110, 7251, 7495, 7685, 7688, 7916, 7997 --- ## 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](./eip-7685.md). 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](./eip-6110.md) 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](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). ### Constants | Name | Value | Description | | --- | --- | --- | | `CREDENTIAL_DEPOSIT_CONTRACT_ADDRESS` | `TBD` | Address of the new deposit contract | | `CREDENTIAL_DEPOSIT_CONTRACT_SALT` | `TBD` | Salt for the deterministic deployment | | `CREDENTIAL_DEPOSIT_CONTRACT_INIT_CODE` | `TBD` | Initialization code for the deterministic deployment | | `CREDENTIAL_DEPOSIT_CONTRACT_RUNTIME_CODE` | `TBD` | Required runtime code | | `CREDENTIAL_DEPOSIT_REQUEST_TYPE` | `TBD` | EIP-7685 request-type byte | | `CREDENTIAL_DEPOSIT_EVENT_SIGNATURE_HASH` | `0x573a3e66153a38882575e538b0f3fc14cc9ea961a3c7d3b647fb345fab5ea1d6` | Event topic defined below | | `INITIAL_ACTIVATION_TIMESTAMP` | `TBD` | Timestamp that enables the new deposit contract and request type | | `BLS_RETIREMENT_TIMESTAMP` | `TBD` | Timestamp that retires BLS deposits | | `SYSTEM_ADDRESS` | `0xfffffffffffffffffffffffffffffffffffffffe` | Caller for protocol system calls | | `SYSTEM_CALL_GAS_LIMIT` | `30_000_000` | Dedicated gas for a system call | | `DEPOSIT_MODE_STORAGE_SLOT` | `0` | Storage slot for the contract mode | | `DEPOSIT_MODE_DISABLED` | `0` | Deposits are disabled | | `DEPOSIT_MODE_BLS_ENABLED` | `1` | BLS deposits are accepted | | `DEPOSIT_MODE_BLS_RETIRED` | `2` | BLS deposits are rejected | | `ENABLE_DEPOSITS_COMMAND` | `0x00` | System command for initial activation | | `RETIRE_BLS_COMMAND` | `0x01` | System command for BLS retirement | | `MIN_DEPOSIT_AMOUNT` | `1_000_000_000` | Minimum deposit amount in gwei | | `MAX_PUBKEY_LENGTH` | `8192` | Maximum `pubkey` length in bytes | | `MAX_CREDENTIAL_METADATA_LENGTH` | `8192` | Maximum `credential_metadata` length in bytes | | `BLS_CREDENTIAL_SCHEME` | `0` | Scheme identifier for BLS deposits | | `BLS_PUBKEY_LENGTH` | `48` | Length of a BLS12-381 public key | | `BLS_CREDENTIAL_METADATA_LENGTH` | `96` | Length 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: ```text keccak256("CredentialDepositEvent(uint8,bytes,bytes32,uint64,bytes)") ``` ### Definitions The **initial activation block** is the first execution block with: ```text block.timestamp >= INITIAL_ACTIVATION_TIMESTAMP ``` The **BLS retirement block** is the first execution block with: ```text block.timestamp >= BLS_RETIREMENT_TIMESTAMP ``` A deposit or request is **BLS** if and only if: ```text 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](./eip-7997.md) factory MUST deploy the contract before initial activation. Its call input MUST be: ```text 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](./eip-6110.md) transition is complete. EIP-6110 defines this point as: ```text state.eth1_deposit_index == state.deposit_requests_start_index ``` ### Contract interface The contract MUST expose this user function: ```solidity 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: ```solidity 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: ```text topics[0] = CREDENTIAL_DEPOSIT_EVENT_SIGNATURE_HASH ``` The event data is the canonical Solidity ABI encoding of: ```text (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: ```text 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: ```text 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](./eip-4788.md) beacon-root call, if active. 2. Perform the [EIP-2935](./eip-2935.md) 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](./eip-7495.md). They use `ProgressiveList` and `ProgressiveByteList` from [EIP-7916](./eip-7916.md). [EIP-7688](./eip-7688.md) defines their use in evolving consensus containers. ```python 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. ```python 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: ```python 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`: ```python 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: ```python def process_non_bls_credential_deposit( state: BeaconState, request: CredentialDepositRequest, ) -> None: return ``` The BLS handler is: ```python 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](./eip-7251.md). Thus, BLS signature validation, deposit churn, and validator activation remain unchanged. The operation order is: ```python # 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. | Phase | New contract | Legacy contract | Execution-layer derivation | Consensus-layer processing | | --- | --- | --- | --- | --- | | Deployment window | Rejects all deposits | Accepts valid legacy deposits | Derives only EIP-6110 requests | Processes only legacy deposits | | Migration period | Accepts deposits under `BLS_ENABLED` | Accepts valid legacy deposits | Converts both event formats into the generalized request type | Processes one ordered generalized list | | Post-retirement | Rejects BLS and accepts valid non-BLS deposits | Still accepts valid legacy deposits | Derives generalized requests only from the new contract | Processes the generalized list | ## Test Cases Implementations MUST pass the [test cases](../assets/eip-9999/test-cases.md). ## Reference Implementation The [Solidity reference implementation](../assets/eip-9999/CredentialDepositContract.sol) 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 Copyright and related rights waived via [CC0](../LICENSE.md).