--- eip: 8413 title: Decouple Stake from Validator Registration description: Separates economic stake creation on the execution layer from validator registration on the consensus layer. author: Kevaundray Wedderburn (@kevaundray) discussions-to: https://ethereum-magicians.org/t/eip-8413-decouple-stake-from-validator-registration/29628 status: Draft type: Standards Track category: Core created: 2026-09-09 requires: 7251 --- ## Abstract This EIP separates stake creation from validator registration. A staking contract on the execution layer accepts ETH, withdrawal credentials, and a versioned authorization commitment, and immediately returns a `StakeID`. A separate registration contract accepts the `StakeID` and registration data. For BLS, the registration data is the validator public key and proof of possession. The consensus layer verifies the registration data before queuing the stake for pending-deposit processing. Top-ups, withdrawals, and consolidations identify the stake by `StakeID` instead of the validator public key. ## Motivation Stake creation currently carries validator keys and proofs tied to consensus credential formats. A staking contract that accepts versioned authorization commitments can support future credential schemes without changing its stake creation interface. Stake owners also need an identifier before validator registration completes. The staking transaction returns a `StakeID` immediately, allowing users and contracts to reference the position without waiting for a validator index. This identifier provides a common reference for top-ups, consolidations, and withdrawals. It persists before registration and after validator exit, while the consensus layer remains authoritative for balances and lifecycle rules. ## 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). ### Overview The staking contract on the execution layer (EL) records stake creation and top-ups in an append-only event tree. A separate registration contract sends registration requests to the consensus layer (CL). The CL maintains stake balances, verifies registration, and manages the validator lifecycle. Staking has two steps: 1. Create stake: send ETH to the staking contract and receive a `StakeID`. 2. Register a validator: submit the `StakeID`, validator credential, and registration proof. The consensus layer verifies them and queues the stake for pending-deposit processing. The existing deposit flow combines these steps. Separating them gives the staker an identifier before validator creation and lets future authorization formats use the same staking contract. ### Stake Creation and Identity A new execution-layer staking contract creates each stake position through this conceptual interface: ```solidity function stake( bytes32 withdrawalCredentials, bytes32 authorizationCommitment ) external payable returns (uint64 stakeId); ``` `authorizationCommitment` is a versioned commitment to validator credentials. The initial version commits to the BLS public key authorized to register for the stake position. Stake creation receives this commitment instead of the validator public key and proof of possession. The credential is revealed and verified later during registration. The staking contract maintains a single append-only Merkle tree of stake events. We call this the stake-event tree. The stake-event tree contains two event types: ```text CREATE_STAKE { withdrawal_credentials authorization_commitment initial_amount } TOP_UP { stake_id amount } ``` For a newly created stake, `StakeID` equals the event index of its `CREATE_STAKE` event: ```text stake_id := event_index(CREATE_STAKE) ``` The stake-event tree has height 40 and capacity for `2**40` leaves, including the reserved migration prefix. Valid stake IDs satisfy `0 <= stake_id < 2**40`. Consensus serialization MUST represent each `StakeID` as an unsigned 40-bit value, including references in stake-related requests. The Solidity interface uses `uint64` for convenience; however, when the EL processes logs, it should check that the top 24 bits are zero. No process should wrap or truncate the `StakeID`. Both stake creation and top-ups consume event-tree capacity. Once `event_count` reaches `2**40`, including the reserved migration prefix, the contract MUST reject both operations. The contract MUST NOT allocate `StakeID`s from a separate counter. Event indices advance for every event. Stake IDs are also monotonically increasing but sparse: ```text tree-index 8 -> TOP_UP(stake_id=3, ...) tree-index 9 -> TOP_UP(stake_id=5, ...) tree-index 10 -> CREATE_STAKE(...) // stake_id = 10 tree-index 11 -> TOP_UP(stake_id=10, ...) tree-index 12 -> TOP_UP(stake_id=3, ...) tree-index 13 -> CREATE_STAKE(...) // stake_id = 13 ``` For a new stake, a valid `StakeID = i` identifies a `CREATE_STAKE` event at `stake_event_tree[i]`. Migrated stakes use their original validator indices, as defined in [Existing validators](#existing-validators). Indices occupied by `TOP_UP` events are not valid `StakeID`s. The `CREATE_STAKE` leaf commits to immutable creation parameters. Each later `TOP_UP` appends an event that references the original `StakeID`. In the existing deposit contract, every deposit, including a top-up, gets a new deposit index. Here, every event gets an index, but only `CREATE_STAKE` creates a new `StakeID`. The exact SSZ or hashing construction remains to be specified. The contract exposes or commits to at least the stake-event tree root and event count. The CL maintains mutable state separately: ```text StakePositionState { withdrawal_credentials authorization_commitment balance validator_index: Optional[ValidatorIndex] } ``` Registration, top-ups, consolidations, withdrawals, and exit-related accounting reference the original `StakeID`. The initial event count follows the migration rules in [Existing validators](#existing-validators). The CL stores each stake's balance by `StakeID`. Validator rewards and penalties update the balance of the associated stake, replacing the existing balance lookup by `ValidatorIndex`. ### Authorization Commitment The authorization commitment identifies the credential permitted to register for the stake. Withdrawal credentials separately control withdrawals. The initial format authorizes validator registration using a BLS public key. Future versions can include role permissions, such as includer-only or PTC-only participation. Those formats and roles are outside this EIP's current scope. An authorization commitment is a versioned, fixed-size value: ```text AuthorizationCommitment := Bytes32 byte 0 authorization format version bytes 1..31 commitment payload ``` The proposed initial construction is: ```text authorization_commitment = VERSION_1 || SHA256(public_key)[1:] ``` The version defines the credential encoding, commitment construction, and proof verification rules. The CL reads it from the stored authorization commitment. ### Role Registration Role registration reveals the preimage of the authorized commitment for a particular `StakeID` and proves that the submitter is allowed to register a validator under this `StakeID`. For BLS, this means supplying proves that they are allowed to register a validator under this `StakeID`. For BLS, this means supplying the proof of possession for the public key in the preimage. An operator submits a request through a separate EL registration contract using this conceptual interface: ```solidity function register( uint64 stakeId, bytes calldata registrationData ) external; ``` `registrationData` contains the credential and proof authorizing registration for the referenced stake. Note that unlike the previous deposit contract, a bad signature doesn't imply the stake is lost. The contract produces the following execution request which gets sent to the CL: ```text ValidatorRegistrationRequest { stake_id registration_data } ``` The CL processes the request in this order: 1. Verify that the stake exists in CL state, has no pending registration or recovery withdrawal, and is not already bound to a validator. 2. Decode `registration_data` according to the version in the stake's authorization commitment. 3. Verify that the revealed authorization data matches the stake's authorization commitment, including any permissions encoded by that version. 4. Verify that the registration proof is valid under the stake's authorization version. 5. Verify that no pending registration reserves the credential and that no existing or exited validator previously used it. After the stake eligibility checks, decoding and cryptographic verification proceed conceptually as follows: ```text stake = state.stakes[stake_id] version = stake.authorization_commitment[0] authorization, registration_proof = decode_registration_data( version, registration_data ) assert compute_authorization_commitment( version, authorization ) == stake.authorization_commitment assert verify_registration_proof( version, authorization, registration_proof ) ``` Successful registration enters pending-deposit processing with the `StakeID` and verified credential. The CL MUST reserve both against duplicate registration. `stake.validator_index` remains unset until queue processing creates the validator and assigns its index. Finalization, processing limits, churn, and activation remain subject to CL rules. Pending-deposit processing MUST NOT credit the stake balance again. Queue encoding and treatment of balance changes while pending remain to be specified. The stake-to-validator binding is permanent. Neither the `StakeID` nor the credential can register another validator after exit, slashing, or full withdrawal. Registration status is authoritative on the CL. The EL MUST NOT expose canonical registration status without an explicit mechanism that returns it from the CL. ### Lifecycle Operations #### Unregistered stake A stake position can exist without successful registration. The operator can delay registration, or registration can fail because of a commitment mismatch, invalid proof of possession, or consumed credential. Withdrawal recovery is described in [Recovery of unregistered stake](#recovery-of-unregistered-stake). #### Top-ups and exits A top-up references an existing stake position: ```solidity function topup( uint64 stakeId ) external payable; ``` The EL appends a `TOP_UP { stake_id, amount }` event to the stake-event tree. This advances the event count while preserving the referenced `StakeID`. We do not change the CL semantics regarding top-ups: | Stake or validator state | Top-up behavior | | --- | --- | | Unregistered | Increase the balance available to the future validator. | | Active | Increase the stake balance normally. | | Exited | Increase the stake balance and make it withdrawable according to CL rules. | #### Consolidations For registered validators, consolidation retains the CL rules introduced by [EIP-7251](./eip-7251.md). Requests identify source and target stakes by `StakeID` instead of validator public keys: ```text StakeConsolidationRequest { source_stake_id target_stake_id } ``` The CL resolves the stake IDs to their associated validators and applies existing authorization, eligibility, exit, churn, and transfer rules. Balance transfers update the corresponding stake balances. Consolidation MUST NOT change either position's `StakeID`, authorization commitment, withdrawal credentials, or validator binding. #### Withdrawals Withdrawals reference the stake position: ```text StakeWithdrawalRequest { stake_id amount } ``` For registered validators, withdrawal rules remain unchanged. Requests identify the stake by `StakeID` instead of the validator public key. #### Recovery of unregistered stake Before registration is accepted into the pending-deposit queue, the withdrawal authority can request recovery of the full stake balance. | Constant | Value | Meaning | | --- | --- | --- | | `MAX_UNREGISTERED_WITHDRAWALS_PER_PAYLOAD` | `8` | Maximum recovery payments per block. | | `PENDING_UNREGISTERED_WITHDRAWALS_LIMIT` | `4096` | Maximum recovery requests waiting in CL state. | - The CL MUST verify that the request's authenticated sender matches the stake's withdrawal address before adding it to the queue. - Requests are immediately eligible, with no mandatory delay. They are processed in acceptance order, up to eight per block and within the overall withdrawal payload limit. Remaining requests wait for a later block. - If the queue is full, the CL MUST ignore new recovery requests. The funds remain in the stake, and the owner can retry later. - The CL MUST reject unregistered-stake recovery after registration enters the pending-deposit queue. Ordinary validator withdrawal rules apply after validator creation. While recovery is queued, the CL MUST reject registration and duplicate recovery requests for that stake. - Recovery preserves the `StakeID` and authorization commitment. Later top-ups can fund registration under the same commitment. - Unregistered stakes MUST NOT be consolidation sources or targets. To fund another stake, the owner first withdraws the ETH, then deposits it through the staking contract. ## Rationale ### Versioned commitments A fixed-size commitment lets the staking contract accept future credential formats without changing its interface. ### Consensus-layer registration The CL holds the state needed to check credential reuse and validator status. Verifying registration there avoids duplicating that state in an EL contract. Using the EL to transport the information to the CL lets operators submit registration through the EL's transaction inclusion mechanisms, including fork-choice enforced inclusion lists (FOCIL, [EIP-7805](./eip-7805.md)), and be rate-limited by EL infrastructure (the gas limit). ### Withdrawal-only recovery Allowing unregistered stakes to consolidate would introduce balance transfers outside the existing validator consolidation rules. Withdrawal recovery returns funds to their owner, who can fund another stake through the staking contract. ## Backwards Compatibility This change requires a coordinated fork, a new staking contract, and updates to staking tools and CL balance accounting. Activation rules remain unchanged. The fork transition must define how pending legacy deposits receive stake IDs and how deposits to the old contract are handled after the fork. ### Existing validators Each existing validator receives a `StakeID` equal to its `ValidatorIndex`, including exited, slashed, and fully withdrawn validators. Migration copies its current balance and withdrawal credentials into the stake position and sets `validator_index`. The validator retains its credential and lifecycle state without another registration or proof of possession. Let `N` be the complete validator registry length at migration, with `N < 2**40`. The tree reserves leaves `[0, N)` as zero-filled placeholders, and the contract initializes `event_count = N`. These leaves contain no creation events. At the fork, the CL creates these stake records from the existing validators and their balances. New events start at index `N`, preventing collisions with migrated stake IDs. The reserved prefix leaves capacity for `2**40 - N` new events. Migrated stake IDs remain valid for top-ups, withdrawals, and consolidations under the applicable CL rules. ## Test Cases TODO ## Security Considerations TODO ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).