This EIP introduces a preregistration mechanism that allows a validator key holder to commit to specific withdrawal credentials before any validator deposit is made, addressing a known deposit front-running vulnerability in delegated staking. Preregistrations are submitted through a new EIP-7685 request contract on the execution layer, following the same queue, fee, and system-call pattern as EIP-7002 and EIP-7251. The consensus layer stores the expirable preregistration in beacon state and enforces it at deposit processing time: matching deposits proceed and the preregistration is consumed; mismatched deposits are silently rejected. The mechanism is fully optional — deposits for public keys without a preregistration are processed exactly as today.
Motivation
In delegated staking, the entity funding a validator and the entity generating the BLS keypair are distinct parties. This separation is common across liquid staking protocols, staking-as-a-service providers, and any arrangement where one party provides capital and another operates the validator. The BLS key is the validator’s identity: the first deposit for a given pubkey establishes the withdrawal credentials permanently under first-deposit-wins semantics. The key holder can therefore submit a deposit with attacker-controlled withdrawal credentials before the funding party’s deposit is processed. The funding party has no on-chain guarantee that the withdrawal credentials it intends will be honored.
At least a third of all staked ETH flows through delegated architectures with identifiable protocol-level protections against this attack. Since no on-chain mechanism exists to bind a public key to withdrawal credentials before the first deposit, every affected product has independently built application-layer defenses — bond-based pre-deposit schemes, guardian committees, or both. These defenses work (no known exploits have occurred in production), but each one is built, audited, and maintained independently, and new entrants are vulnerable by default until they do the same.
Preregistration addresses this at the protocol layer: the key holder signs a binding commitment to specific withdrawal credentials before any validator deposit is made, and the consensus layer enforces this commitment at deposit processing time.
For an extended analysis of the problem scope, existing application-layer defenses, and data sources, see the discussion thread linked in the header.
Specification
Configuration
Execution layer
Name
Value
Comment
PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS
TBD
Where to call and store relevant details about preregistration mechanism
PREREGISTRATION_REQUEST_TYPE
TBD
The EIP-7685 type prefix for preregistration request
SYSTEM_ADDRESS
0xfffffffffffffffffffffffffffffffffffffffe
Address used to invoke system operation on contract
EXCESS_PREREGISTRATION_REQUESTS_STORAGE_SLOT
0
PREREGISTRATION_REQUEST_COUNT_STORAGE_SLOT
1
PREREGISTRATION_REQUEST_QUEUE_HEAD_STORAGE_SLOT
2
Pointer to head of the preregistration request message queue
PREREGISTRATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT
3
Pointer to the tail of the preregistration request message queue
PREREGISTRATION_REQUEST_QUEUE_STORAGE_OFFSET
4
The start memory slot of the in-state preregistration request message queue
MAX_PREREGISTRATION_REQUESTS_PER_BLOCK
4
Maximum number of preregistration requests that can be dequeued into a block
TARGET_PREREGISTRATION_REQUESTS_PER_BLOCK
1
MIN_PREREGISTRATION_REQUEST_FEE
1
PREREGISTRATION_REQUEST_FEE_UPDATE_FRACTION
17
EXCESS_INHIBITOR
2**256-1
Excess value that blocks requests before activation and while disabled
Consensus layer
Name
Value
Comment
DOMAIN_PREREGISTRATION
DomainType('0x11000000')
Uses GENESIS_FORK_VERSION (stable across forks)
MAX_PREREGISTRATION_REQUESTS_PER_PAYLOAD
Uint64(2**2) (= 4)
Maximum preregistration requests per execution payload
PREREGISTRATIONS_LIMIT
Uint64(2**19) (= 524,288)
State-transition limit on active preregistrations
PREREGISTRATION_EXPIRY_SLOTS
Slot(2**18) (= 262,144)
Lifetime in slots (~36 days at 12 seconds per slot)
Execution layer
Definitions
FORK_BLOCK — the first block in a blockchain after this EIP has been activated.
Preregistration request operation
The new preregistration request is an EIP-7685 request with type PREREGISTRATION_REQUEST_TYPE and consists of the following fields:
pubkey: Bytes48
withdrawal_credentials: Bytes32
signature: Bytes96
The EIP-7685 encoding of a preregistration request is computed as follows.
Each serialized request record is 176 bytes (pubkey ++ withdrawal_credentials ++ signature), and request_data is the flat concatenation of all dequeued records in queue order.
Preregistration Request Contract
The contract has three different code paths, which can be summarized at a high level as follows:
System process - if called by system address, pop off the preregistration requests for the current block from the queue. If the call carries non-empty calldata, additionally disable the queue (see Rationale).
Add preregistration request - for any other caller, requires a 176 byte input, the validator’s public key concatenated with withdrawal credentials and a BLS signature. An accepted request is emitted verbatim as an anonymous log (LOG0, no topics).
Fee getter - for any other caller, if the input length is zero, return the current fee required to add a preregistration request.
For non-system callers, any other input length MUST cause the call to revert.
Add Preregistration Request
If call data input to the contract is exactly 176 bytes, perform the following:
Ensure enough ETH was sent to cover the current preregistration request fee (check_fee())
Increase preregistration request count by 1 for the current block (increment_count())
Insert a preregistration request into the queue for the pubkey, withdrawal credentials, and signature (insert_preregistration_request_into_queue())
Emit the accepted pubkey ++ withdrawal_credentials ++ signature request as an anonymous log
Specifically, the functionality is defined in pseudocode as the function add_preregistration_request():
defadd_preregistration_request(Bytes48:pubkey,Bytes32:withdrawal_credentials,Bytes96:signature):"""
Add preregistration request adds new request to the preregistration request queue,
so long as a sufficient fee is provided.
"""# Verify sufficient fee was provided.
fee=get_fee()require(msg.value>=fee,'Insufficient value for fee')# Increment preregistration request count.
count=sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_COUNT_STORAGE_SLOT)sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_COUNT_STORAGE_SLOT,count+1)# Insert into queue.
queue_tail_index=sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT)queue_storage_slot=PREREGISTRATION_REQUEST_QUEUE_STORAGE_OFFSET+queue_tail_index*6sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot,pubkey[0:32])sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+1,pubkey[32:48]++withdrawal_credentials[0:16])sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+2,withdrawal_credentials[16:32]++signature[0:16])sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+3,signature[16:48])sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+4,signature[48:80])sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+5,signature[80:96])log0(pubkey++withdrawal_credentials++signature)sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT,queue_tail_index+1)
Fee calculation
The following pseudocode can compute the cost of an individual preregistration request, given a certain number of excess preregistration requests.
defget_fee()->int:excess=sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,EXCESS_PREREGISTRATION_REQUESTS_STORAGE_SLOT)require(excess!=EXCESS_INHIBITOR,'Inhibitor still active')returnfake_exponential(MIN_PREREGISTRATION_REQUEST_FEE,excess,PREREGISTRATION_REQUEST_FEE_UPDATE_FRACTION)deffake_exponential(factor:int,numerator:int,denominator:int)->int:i=1output=0numerator_accum=factor*denominatorwhilenumerator_accum>0:output+=numerator_accumnumerator_accum=(numerator_accum*numerator)//(denominator*i)i+=1returnoutput//denominator
As in EIP-7002 and EIP-7251, get_fee() uses only the persisted excess value. It does not include requests accumulated in count during the current block. The end-of-block system call incorporates count into excess, so requests submitted in block N affect the fee starting in block N + 1.
Fee Getter
When the input to the contract is length zero and the caller is not SYSTEM_ADDRESS, interpret this as a get request for the current fee. The call MUST carry no value and returns the result of get_fee() without modifying state; a value-bearing fee getter call MUST revert.
System Call
At the end of processing any execution block starting from the FORK_BLOCK, call PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS as SYSTEM_ADDRESS with no calldata. The invocation triggers the following:
The contract’s queue is updated based on preregistration requests dequeued and the preregistration requests queue head/tail are reset if the queue has been cleared (dequeue_preregistration_requests())
The contract’s excess preregistration requests are updated based on usage in the current block (update_excess_preregistration_requests())
The contract’s preregistration requests count is reset to 0 (reset_preregistration_requests_count())
Each preregistration request must appear in the EIP-7685 requests list in the exact order returned by dequeue_preregistration_requests().
Additionally, the system call and the processing of that block must conform to the following:
The call has a dedicated gas limit of 30_000_000.
Gas consumed by this call does not count against the block’s overall gas usage.
Both the gas limit assigned to the call and the gas consumed are excluded from any checks against the block’s gas limit.
The call does not follow EIP-1559 fee burn semantics — no value should be transferred as part of this call.
If there is no code at PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS, the corresponding block MUST be marked invalid.
If the call to the contract fails or returns an error, the block MUST be invalidated.
A system call with non-empty calldata dequeues requests as usual but then stores EXCESS_INHIBITOR into the excess slot, disabling the queue: subsequent preregistration requests revert until a system call with empty calldata re-enables it. Under this EIP the system call is always made with empty calldata, so this path is unreachable; it is reserved for a future upgrade (see Rationale).
The functionality triggered by the system call is defined in pseudocode as the function read_preregistration_requests():
###################
# Public function #
###################
defread_preregistration_requests():reqs=dequeue_preregistration_requests()update_excess_preregistration_requests()reset_preregistration_requests_count()returnssz.serialize(reqs)###########
# Helpers #
###########
classPreregistrationRequest(object):pubkey:Bytes48withdrawal_credentials:Bytes32signature:Bytes96defdequeue_preregistration_requests():queue_head_index=sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_QUEUE_HEAD_STORAGE_SLOT)queue_tail_index=sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT)num_in_queue=queue_tail_index-queue_head_indexnum_dequeued=min(num_in_queue,MAX_PREREGISTRATION_REQUESTS_PER_BLOCK)reqs=[]foriinrange(num_dequeued):queue_storage_slot=PREREGISTRATION_REQUEST_QUEUE_STORAGE_OFFSET+(queue_head_index+i)*6pubkey=(sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot)[0:32]+sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+1)[0:16])withdrawal_credentials=(sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+1)[16:32]+sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+2)[0:16])signature=(sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+2)[16:32]+sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+3)[0:32]+sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+4)[0:32]+sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,queue_storage_slot+5)[0:16])req=PreregistrationRequest(pubkey=Bytes48(pubkey),withdrawal_credentials=Bytes32(withdrawal_credentials),signature=Bytes96(signature))reqs.append(req)new_queue_head_index=queue_head_index+num_dequeuedifnew_queue_head_index==queue_tail_index:# Queue is empty, reset queue pointers
sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_QUEUE_HEAD_STORAGE_SLOT,0)sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT,0)else:sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_QUEUE_HEAD_STORAGE_SLOT,new_queue_head_index)returnreqsdefupdate_excess_preregistration_requests():# A system call with non-empty calldata disables the queue (see Rationale)
iflen(msg.data)>0:sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,EXCESS_PREREGISTRATION_REQUESTS_STORAGE_SLOT,EXCESS_INHIBITOR)returnprevious_excess=sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,EXCESS_PREREGISTRATION_REQUESTS_STORAGE_SLOT)# Reset excess to 0 on the first system call after activation and when
# re-enabling a disabled queue
ifprevious_excess==EXCESS_INHIBITOR:previous_excess=0count=sload(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_COUNT_STORAGE_SLOT)new_excess=0ifprevious_excess+count>TARGET_PREREGISTRATION_REQUESTS_PER_BLOCK:new_excess=previous_excess+count-TARGET_PREREGISTRATION_REQUESTS_PER_BLOCKsstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,EXCESS_PREREGISTRATION_REQUESTS_STORAGE_SLOT,new_excess)defreset_preregistration_requests_count():sstore(PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS,PREREGISTRATION_REQUEST_COUNT_STORAGE_SLOT,0)
Bytecode
TBD — the runtime bytecode will be provided with the reference implementation.
Deployment
TBD — the deterministic deployment transaction will be provided with the reference implementation.
Consensus layer
A sketch of the key consensus layer changes is included below.
New types
classPreregistrationRequests(ProgressiveList[PreregistrationRequest]):"""The preregistration requests pertaining to a single execution payload."""classValidatorPreregistrations(ProgressiveList[StoredPreregistration]):"""The stored preregistrations, including expired records not yet garbage-collected."""
New containers
classValidatorPreregistration(Container):pubkey:BLSPubkey# 48 bytes — the validator key being registered
withdrawal_credentials:Bytes32# 32 bytes — withdrawal credentials to lock in
classPreregistrationRequest(Container):pubkey:BLSPubkey# 48 bytes
withdrawal_credentials:Bytes32# 32 bytes
signature:BLSSignature# 96 bytes — BLS proof of key ownership
classStoredPreregistration(Container):pubkey:BLSPubkeywithdrawal_credentials:Bytes32expiry_slot:Slot# absolute expiry slot, set when the preregistration is stored
Modified containers
ExecutionRequests is extended with a new field:
preregistrations:PreregistrationRequests# [New]
Preregistration requests are applied from parent_execution_requests after deposit requests — under EIP-7732, a payload’s execution requests are carried in the next block and applied while processing it. The number of preregistration requests in a payload MUST NOT exceed MAX_PREREGISTRATION_REQUESTS_PER_PAYLOAD.
defget_stored_preregistration_index(state:BeaconState,pubkey:BLSPubkey)->Optional[Uint64]:forindex,pre_reginenumerate(state.validator_preregistrations):ifpre_reg.pubkey==pubkey:returnUint64(index)returnNonedefis_active_preregistration(state:BeaconState,pre_reg:StoredPreregistration)->bool:# Activity is evaluated against the outstanding parent payload, whose
# execution requests are the ones being applied during block processing
parent_slot=state.latest_execution_payload_bid.slotreturnparent_slot<pre_reg.expiry_slotdefremove_stored_preregistration(state:BeaconState,pubkey:BLSPubkey)->None:state.validator_preregistrations=ValidatorPreregistrations([pre_regforpre_reginstate.validator_preregistrationsifpre_reg.pubkey!=pubkey])defis_pending_validator(pending_deposits:Sequence[PendingDeposit],pubkey:BLSPubkey)->bool:forpending_depositinpending_deposits:ifpending_deposit.pubkey!=pubkey:continueifis_valid_deposit_signature(pending_deposit.pubkey,pending_deposit.withdrawal_credentials,pending_deposit.amount,pending_deposit.signature,):returnTruereturnFalse
Note: Clients SHOULD maintain a secondary index by pubkey for efficient lookup rather than performing a linear scan.
Process preregistration request
defprocess_preregistration_request(state:BeaconState,preregistration_request:PreregistrationRequest,)->None:pubkey=preregistration_request.pubkeywithdrawal_credentials=preregistration_request.withdrawal_credentialssignature=preregistration_request.signatureindex=get_stored_preregistration_index(state,pubkey)# Reject if an active preregistration already exists for this pubkey
ifindexisnotNoneandis_active_preregistration(state,state.validator_preregistrations[index]):return# Reject if a validator with this pubkey already exists
ifpubkeyin[v.pubkeyforvinstate.validators]:return# Reject if this pubkey already has a valid pending deposit
ifis_pending_validator(state.pending_deposits,pubkey):return# Reject if active records are at capacity. Only active records count, so
# the timing of the garbage-collection sweep does not affect admission.
# The check applies to appends and to replacements alike, since both
# create an active binding
active_count=len([pre_regforpre_reginstate.validator_preregistrationsifis_active_preregistration(state,pre_reg)])ifactive_count>=PREREGISTRATIONS_LIMIT:return# Verify BLS signature: proof of key ownership.
# Domain uses GENESIS_FORK_VERSION (fork_version=None default) + genesis_validators_root.
preregistration=ValidatorPreregistration(pubkey=pubkey,withdrawal_credentials=withdrawal_credentials,)domain=compute_domain(DOMAIN_PREREGISTRATION,genesis_validators_root=state.genesis_validators_root,)signing_root=compute_signing_root(preregistration,domain)ifnotbls.Verify(pubkey,signing_root,signature):returnstored=StoredPreregistration(pubkey=pubkey,withdrawal_credentials=withdrawal_credentials,expiry_slot=Slot(state.slot+PREREGISTRATION_EXPIRY_SLOTS),)ifindexisnotNone:# Replace the expired record in place
state.validator_preregistrations[index]=storedelse:state.validator_preregistrations.append(stored)
Modified deposit request ingestion
process_deposit_request (EIP-6110) is modified to enforce preregistration constraints for validator deposits. Builder deposits use a separate request type and process_builder_deposit_request. When an active preregistration exists, the validator deposit is accepted only if both the withdrawal credentials match and the deposit BLS signature is valid. This early BLS check prevents an attacker from consuming a preregistration with an invalid-signature deposit (see Security Considerations).
defprocess_deposit_request(state:BeaconState,deposit_request:DepositRequest,)->None:# Only an active binding is enforced; an expired record is ignored
index=get_stored_preregistration_index(state,deposit_request.pubkey)ifindexisnotNoneandis_active_preregistration(state,state.validator_preregistrations[index]):pre_reg=state.validator_preregistrations[index]ifdeposit_request.withdrawal_credentials!=pre_reg.withdrawal_credentials:return# Withdrawal credentials mismatch: reject
ifnotis_valid_deposit_signature(deposit_request.pubkey,deposit_request.withdrawal_credentials,deposit_request.amount,deposit_request.signature,):return# Invalid BLS sig: reject, preserve preregistration
remove_stored_preregistration(state,deposit_request.pubkey)state.pending_deposits.append(PendingDeposit(pubkey=deposit_request.pubkey,withdrawal_credentials=deposit_request.withdrawal_credentials,amount=deposit_request.amount,signature=deposit_request.signature,slot=state.slot,))
The key invariant: a valid pending new-validator deposit cannot coexist with an active preregistration for the same pubkey. An invalid-signature PendingDeposit may coexist when it was queued before the preregistration and is ignored by normal pending-deposit processing. apply_pending_deposit requires no modifications.
The sweep runs during epoch processing (process_epoch) and is garbage collection only: once stored, a binding remains active for subsequently processed payloads whose slots are below its stored expiry_slot, even if the expired record remains stored until the next sweep (see Rationale).
Rationale
Enforcement at deposit ingestion
Invalid deposits are already silently ignored by the consensus layer if the signature is invalid. This EIP adds one optional validation condition: if an active preregistration exists for a pubkey, only deposits with matching withdrawal credentials and a valid BLS signature are accepted. Deposits with mismatched credentials or invalid signatures are treated as invalid and ignored. The check only applies to pubkeys with an active preregistration — all other deposits are processed as before.
Permanent rejection of mismatched deposits
When a deposit’s withdrawal credentials do not match an active preregistration, the deposit is silently rejected — the ETH remains in the Deposit Contract, which has no withdrawal function. This is a deliberate design choice: returning mismatched deposits would allow an attacker to front-run at no cost (deposit, get rejected, recover funds, repeat). Permanent loss makes front-running economically self-defeating.
The risk of accidental mismatch is low in practice. Preregistration requires a dedicated BLS signing step separate from deposit data. Staking protocols verify the preregistration on-chain via EIP-4788 before submitting deposits. At the current 12-second slot duration, preregistrations expire automatically after ~36 days if unused, freeing beacon state.
System contract vs CL gossip
CL gossip has no economic cost — generating BLS keypairs is cheap (~2ms), and verification cost (~1.5ms per message) falls on every node, creating an asymmetry favoring an attacker. The EL system contract adds an EIP-1559-style fee that starts at 1 wei and increases exponentially in response to sustained demand above the target rate. The fee update takes effect in the following block. Staking protocols can call the system contract directly from their own contracts.
Expiry keyed to the outstanding payload slot
Expiry is enforced logically through is_active_preregistration, which compares the outstanding parent bid’s slot against the stored expiry_slot, not against the slot in which the requests are applied. Under EIP-7732 a payload’s requests are applied one block later, so this guarantees that a deposit is checked against the bindings that were active for the slot in which its payload was created. The comparison is strict: a payload at expiry_slot is no longer protected. The epoch-processing sweep is pure garbage collection: its timing does not affect whether a binding is enforced or a request is admitted. Consequently, an expired record may remain physically present until the next sweep and can be replaced by a new preregistration in place, subject to the active-capacity check.
The absolute deadline is computed once, when the preregistration is stored. This prevents a future change to PREREGISTRATION_EXPIRY_SLOTS from retroactively changing existing deadlines and makes the same window exactly verifiable on the execution layer without knowing the lifetime parameter, slot duration, or SLOTS_PER_EPOCH: a verification contract proves expiry_slot through EIP-4788 and requires the current EIP-7843SLOTNUM to be below it, making the deposit atomically either protected or reverted. Expiry does not depend on finality and therefore continues while finality stalls. A binding that expires unused can be re-established by replaying the same signed message.
Domain choice
DOMAIN_PREREGISTRATION uses GENESIS_FORK_VERSION (the default when no fork_version is passed to compute_domain), so a preregistration signed once is valid across all past and future forks. This allows signing on air-gapped hardware without knowledge of the current fork version. Chain separation is achieved via genesis_validators_root, which is an immutable per-chain constant.
A dedicated domain is used rather than reusing DOMAIN_DEPOSIT because DOMAIN_DEPOSIT does not include genesis_validators_root, meaning deposit-domain signatures can be replayed across chains.
BLS verification on CL only
BLS verification happens on the CL, not in the EL system contract. The signing domain includes genesis_validators_root, and the CL must perform semantic validation before storing the preregistration. Repeating the verification on the EL would duplicate that work, so the system contract acts only as a rate-limited queue. Submission is permissionless, and the BLS signature is the sole authorization mechanism. Invalid signatures are silently discarded by the CL, costing only the system contract fee.
Disabling the queue
A system call with non-empty calldata stores EXCESS_INHIBITOR into the excess slot and thereby disables the queue; a later system call with empty calldata restores it, through the same code path that clears the inhibitor at activation. Under this EIP every system call is made with empty calldata, so the path is unreachable. It exists for a future upgrade that retires or suspends the contract: without it, once the protocol stops making the per-block system call, the contract would keep accepting requests and fees into a queue that never drains. The EIP-8282 builder request contracts’ reference implementation uses the same mechanism.
Parameter choices
TARGET_PREREGISTRATION_REQUESTS_PER_BLOCK = 1 keeps the baseline fee minimal for an infrequent operation. MAX_PREREGISTRATION_REQUESTS_PER_BLOCK = 4 allows up to four queued requests to be dequeued into each block; it does not cap the number of submissions accepted by the contract within a block.
PREREGISTRATIONS_LIMIT = 524,288 (2^19) is chosen so that active capacity cannot be exhausted at the target rate. At 1 preregistration per block, accumulating 2^19 active entries takes ~72.8 days at 12 seconds per slot — well beyond the 2^18-slot expiry window (~36 days at 12 seconds per slot). In steady state at target rate, the active count stabilizes around 2^18 entries (~50% of the limit) as new arrivals are balanced by expiring entries. Sustained saturation requires an average rate above target, which causes excess to accumulate and the fee to grow exponentially — making prolonged spam economically prohibitive. Worst-case state size is ~46 MB of active records (88 bytes × 524,288 entries), plus bounded headroom for expired records awaiting the next epoch’s sweep — at most MAX_PREREGISTRATION_REQUESTS_PER_PAYLOAD × (SLOTS_PER_EPOCH − 1) = 124 additional records.
Deposit process for staking protocols
Protocols that already have their own defenses can adopt preregistration at their own pace, or not at all. For protocols that do adopt it, the intended flow is: the operator signs a preregistration, the protocol submits it to the system contract, verifies its presence in beacon state on-chain via an EIP-4788 Merkle proof, and submits the deposit. When the verification and the deposit are combined into a single transaction — using a shared verification contract, not part of this specification but provided separately as a reference implementation — no finality wait is needed: on any fork branch that lacks the preregistration, the proof fails and the transaction reverts. When verification is a separate transaction, the protocol MUST wait until a beacon state containing the preregistration is finalized before depositing. In both flows, the deposit transaction MUST revert unless the current EIP-7843SLOTNUM is below the proven expiry_slot: an expired record may remain provable in beacon state until it is garbage-collected, while the consensus layer no longer enforces it.
Backwards Compatibility
This EIP introduces backward-incompatible changes to the block structure and validation rules on both the consensus and execution layers, and must be scheduled with a hard fork.
Execution layer: a new system contract is deployed and a new EIP-7685 request type is introduced.
Consensus layer: the ExecutionRequests container and BeaconState are extended with new fields.
Test Cases
TBD — test vectors will be provided with the reference implementation.
Reference Implementation
TBD
Security Considerations
Impact on existing deposit workflows
Preregistration is fully optional. Without a preregistration for a given pubkey, deposits are processed exactly as today — the first-deposit-wins rule applies unchanged. Existing validators, top-up deposits, and non-delegated staking are entirely unaffected.
Non-delegated staking (solo stakers, exchanges, institutions) controls both funds and keys, so the front-running vulnerability does not apply. These stakers may optionally preregister, but gain no benefit from doing so.
Bond-based protocols that verify withdrawal credentials via EIP-4788 after a pre-deposit can adopt preregistration to skip the verification delay and avoid locking capital in the entry queue. Their existing flow continues to work without changes.
Guardian-committee protocols that rely on depositRoot snapshot signing would need to update their deposit flow. Preregistration replaces the guardian infrastructure with a protocol-level guarantee, eliminating depositRoot rotation attacks and committee liveness dependencies.
Preregistration signature is permanent
The preregistration signature is fork-agnostic and does not expire cryptographically — only the on-chain record has a 262,144-slot validity window (~36 days at the current 12-second slot duration). Anyone who obtains a signed preregistration message can resubmit it at any time to re-establish the on-chain record after expiry. Once an operator signs a preregistration binding a pubkey to specific withdrawal credentials, that commitment is effectively permanent. The protocol does not restrict credential prefixes beyond the existing validator deposit rules, so operators must ensure that the selected credentials are supported by their intended withdrawal flow.
Builder deposits
Preregistration applies only to validator deposits. Builder deposits (EIP-8282) use an independent request type and registry: a preregistration may coexist with a builder for the same pubkey and is neither checked nor consumed by builder deposits. It MUST NOT be treated as authorization of a builder’s execution address. In particular, preregistering 0xB0 withdrawal credentials does not register or protect a builder; submitted through the validator deposit path, matching 0xB0 credentials create a validator that cannot currently withdraw.
Race between preregistration and deposit
If an attacker deposits for a pubkey before the preregistration is processed, the deposit creates a validator under first-deposit-wins and the preregistration is rejected. If both arrive in the same execution payload, deposits are processed before preregistrations, so the same outcome applies. Staking protocols detect this via EIP-4788: they MUST verify an active preregistration — both its presence in beacon state and its expiry deadline — atomically with the deposit. When verifying in a separate transaction instead, they MUST wait until its inclusion is finalized and enforce the deadline in the deposit transaction.
Invalid-signature deposit attack
An adversary could attempt to strip preregistration protection by depositing with matching withdrawal credentials but an invalid BLS signature. This EIP prevents this attack by verifying the deposit’s BLS signature in process_deposit_request before consuming the preregistration. Only deposits with both matching withdrawal credentials and a valid BLS signature can consume a preregistration. Invalid-signature deposits are silently rejected without affecting the preregistration.
DoS and state growth
The EIP-1559-style fee mechanism is the primary defense against sustained above-target spam. Every accepted submission in block N increments count, while all submissions in that block face the same required minimum fee computed from the excess stored before transaction processing. At the end of block N, the system call updates excess to max(0, previous_excess + count − TARGET_PREREGISTRATION_REQUESTS_PER_BLOCK); the resulting fee, which approximates e**(excess / 17) wei, applies in block N + 1. Thus, a high-volume block does not increase the fee for later submissions in the same block, but it does increase the fee for requests in subsequent blocks. At a sustained rate of n accepted submissions per block where n > 1, excess grows by n − 1 per block and the fee exceeds 1 ETH after roughly 17 × ln(10^18) / (n − 1) blocks.
Under normal operation, the state list stays near zero (preregistrations consumed within hours). However, starting from zero excess, one accepted request per block keeps the fee at its minimum and can maintain approximately 2^18 active entries — about 23 MB of serialized records plus client overhead. The 524,288 active-entry limit is sized so that saturation requires a sustained rate above target — and therefore an exponentially growing fee.
At exactly the target rate, excess and the current fee do not decay. Maintaining an elevated excess still requires paying the elevated fee on every request; once demand falls below target, excess decays. The fee is a rate limiter, not a guarantee of low-cost availability.
Censorship resistance
The 262,144-slot validity window (~36 days at 12 seconds per slot) is deliberately generous because the workflow spans preregistration, operational coordination, proof availability, and a deposit transaction. The lifetime is intentionally defined in slots, so a future slot-duration change changes its wall-clock duration without changing its consensus or contract interpretation. A much shorter window would make completion more sensitive to temporary censorship, congestion, or high fees; replaying the signature restarts that flow rather than eliminating it.
Fee overpayment
The system contract does not refund excess fee payment. Callers should query the current fee via the fee getter (empty calldata) before submitting. This is the same behavior as EIP-7002.
System call failure
If the system call to the preregistration contract fails for any reason, the block MUST be deemed invalid. This is the same consideration as in EIP-7002.
Empty code failure
If there is no code at PREREGISTRATION_REQUEST_PREDEPLOY_ADDRESS, the block MUST be deemed invalid. This is the same consideration as in EIP-7002.