This EIP adds a post-quantum public key registry to the beacon state. Each validator registers two hash-based commitments: a leanXMSS key, which will sign all its messages once duties switch to it, and the 32-byte root of a verifiable random function (VRF) tree, which fixes one verifiable random output per index. Each builder registers a leanXMSS key and a zero VRF root.
Registration is a new beacon block operation carrying the keys and a proof of possession that binds the leanXMSS key to the registrant’s BLS public key. The registrant’s BLS signing key authorizes it and can replace registered keys, at a limited rate. The keys stay dormant until a later fork switches duties to them.
Motivation
Every consensus-layer signature today is a BLS12-381 signature: attestations, block proposals, RANDAO reveals, aggregation selection proofs, sync committee messages, payload attestations, inclusion lists, proposer preferences, voluntary exits, the deposit proof of possession, and the execution payload bids and envelopes of builders. BLS relies on the hardness of discrete logarithms in pairing-friendly groups, which Shor’s algorithm breaks. A cryptographically relevant quantum computer (CRQC) recovers a secret key from its public key, and every validator’s and builder’s public key is in the beacon state.
BLS therefore has to be replaced, and this EIP splits the replacement into two forks. Generating a post-quantum key involves the validator’s secret key material, requires new tooling, and, for staking pools, requires coordination between node operators and stake owners. If this work happens at the fork that switches the signature scheme, the number of ready validators is only known at that fork.
A separate registry fork has the following benefits:
Preparation. Validators generate and register keys, with tooling tested on mainnet, while BLS remains the only scheme used for duties.
A readiness signal. The effective balance of active validators with registered keys can be computed from the state, so the switch can be scheduled against an observed threshold.
Early implementation. Clients implement the leanXMSS verifier and the new containers on mainnet before any duty depends on them, since every registration carries a proof of possession.
A recovery anchor. If a CRQC appears to be near before the switch, a fork can stop key changes. Keys registered before that fork, while BLS was believed secure, are then the keys the chain moves to, without relying on any BLS signature made afterwards.
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.
The changes apply to the consensus specifications (ethereum/consensus-specs) on top of Heze, and are rebased onto the fork that includes this EIP once it is scheduled. The fork that activates this EIP is assigned by the relevant meta EIP. Functions and constants not defined here keep their meaning from those specifications. The registrant of a registration is the validator or builder whose index it names.
Cryptographic helpers
This EIP uses two hash-based primitives, a stateful signature scheme and a verifiable random function (VRF), through the interface below. It specifies the interface and the properties it relies on, not the constructions, so that the registry does not depend on how the primitives are built (see Interface instead of construction). Key generation, signing, and opening are not consensus-critical.
The signature scheme is leanXMSS and the VRF is a hash-based VRF tree. leanXMSS, with 32-byte public keys and 2404-byte signatures, is implemented in the Cryptography/Xmss module of the ethereum/EthLib repository at commit 6390c73ef040311e807b9047d99dca6b1be1c42f.
A VRF tree is a hash tree whose leaves commit to secret values, the leaf secrets, held by the validator. The root commits to all of them. Opening an index reveals its leaf secret with the authentication path to the root, and the output at that index is a hash of the leaf secret.
Return whether signature is a valid signature of message under public_key, at the leaf index it carries. Return False, without raising, for a key or signature that does not decode
Return the output of the VRF tree with root root at index if opening is a valid opening of it, and None otherwise. This EIP does not call it
A signature carries its leaf index in its first 4 bytes, little-endian. The proof-of-possession check reads it with:
defget_xmss_signature_index(signature:XMSSSignature)->Uint32:"""
Return the leaf index that a signature carries.
"""returnUint32(int.from_bytes(signature[0:4],ENDIANNESS))
This EIP relies on the following properties:
Stateful signatures. Signatures are unforgeable as long as each leaf index of a key signs at most once. Signing is not assumed to be deterministic, so signing the same message twice at one leaf index is also a reuse. Every leaf index in [0, 2**32) can be used.
Unique VRF outputs. For a given root and index, at most one output verifies, unless a hash collision is known. An output is unpredictable to others until it is opened.
Constants
Domains
Name
Value
Description
DOMAIN_PQ_KEY_REGISTRATION
DomainType('0x12000000')
BLS authorization of a registration by the registrant’s signing key
DOMAIN_PQ_KEY_POSSESSION
DomainType('0x13000000')
Proof of possession under the registered leanXMSS key
Both domains are computed against the genesis fork version, as for BLSToExecutionChange, so that registrations remain valid across forks.
Registry
Name
Value
Description
PQ_POSSESSION_XMSS_INDEX
Uint32(0)
Leaf index at which the proof of possession is signed
Preset
Name
Value
Description
MAX_PQ_KEY_REGISTRATIONS
Uint64(2**3) (= 8)
Maximum registrations per beacon block
MIN_PQ_KEY_CHANGE_DELAY
Epoch(2**8) (= 256)
Minimum number of epochs between two changes of the keys of an index
The minimal preset sets MIN_PQ_KEY_CHANGE_DELAY to Epoch(2**3) (= 8). In every preset, MIN_PQ_KEY_CHANGE_DELAY MUST be at least 1 (see Changes and replay protection), and the Gloas configuration value MIN_BUILDER_WITHDRAWABILITY_DELAY MUST be at least 1 (see Builder index reuse).
Custom types
Name
SSZ equivalent
Description
XMSSPublicKey
ByteVector[32]
A leanXMSS public key
XMSSSignature
ByteVector[2404]
A leanXMSS signature
VRFRoot
Bytes32
The root of a VRF tree
Containers
New containers
classPQKeyMaterial(Container):"""
The post-quantum keys of one validator or builder, as stored in the registry.
Serializes to 32 + 32 = 64 bytes.
"""# leanXMSS public key.
xmss_public_key:XMSSPublicKey# Root of the VRF tree, or zero for a builder.
vrf_root:VRFRoot
classPQKeyRegistration(Container):"""
The message of a registration: which registrant, which keys, and after which change.
"""# Validator index, or builder index with BUILDER_INDEX_FLAG set, the keys are registered for.
index:ValidatorIndex# Keys being registered.
key_material:PQKeyMaterial# Epoch of the last change, or the never-registered marker of the index:
# `FAR_FUTURE_EPOCH` for a validator, the deposit epoch for a builder.
previous_change_epoch:Epoch
classPQKeyPossession(Container):"""
The message of a proof of possession: which registrant, and which leanXMSS key.
"""# BLS public key of the registrant.
pubkey:BLSPubkey# Key whose possession is proven.
xmss_public_key:XMSSPublicKey
classSignedPQKeyRegistration(Container):"""
A registration as it appears in a beacon block.
"""# Registration authorized by signature.
message:PQKeyRegistration# leanXMSS signature of PQKeyPossession at leaf index PQ_POSSESSION_XMSS_INDEX.
possession:XMSSSignature# BLS signature of message by the registrant's signing key.
signature:BLSSignature
New list types
classValidatorPQKeyMaterial(ProgressiveList[PQKeyMaterial]):"""
Post-quantum keys of each validator, by validator index.
A validator without keys holds the all-zero entry.
"""
classValidatorPQKeyChangeEpochs(ProgressiveList[Epoch]):"""
Epoch of the last change of each validator's keys, by validator index.
`FAR_FUTURE_EPOCH` marks a validator that has never registered keys.
"""
classBuilderPQKeyMaterial(ProgressiveList[PQKeyMaterial]):"""
Post-quantum keys of each builder, by builder index.
A builder without keys holds the all-zero entry.
"""
classBuilderPQKeyChangeEpochs(ProgressiveList[Epoch]):"""
Epoch of the last change of each builder's keys, by builder index.
The builder's deposit epoch marks a builder that has never registered keys.
"""
classPQKeyRegistrations(ProgressiveList[SignedPQKeyRegistration]):"""
Post-quantum key registrations included in a beacon block.
"""
The lists are progressive (EIP-7916), following the convention of the Gloas and Heze state.
Modified containers
BeaconBlockBody gains one field, appended after the existing fields:
classBeaconBlockBody(ProgressiveContainer):ACTIVE_FIELDS=active_fields(width=14,gaps=(1,6))# [Modified in this EIP]
# ... existing fields ...
pq_key_registrations:PQKeyRegistrations# [New in this EIP]
BeaconState gains four fields, appended after the existing fields. The Validator and Builder containers are unchanged.
classBeaconState(ProgressiveContainer):ACTIVE_FIELDS=active_fields(width=50,gaps=(8,9,10,28))# [Modified in this EIP]
# ... existing fields ...
post_quantum_key_material:ValidatorPQKeyMaterial# [New in this EIP]
post_quantum_key_change_epochs:ValidatorPQKeyChangeEpochs# [New in this EIP]
builder_post_quantum_key_material:BuilderPQKeyMaterial# [New in this EIP]
builder_post_quantum_key_change_epochs:BuilderPQKeyChangeEpochs# [New in this EIP]
The ACTIVE_FIELDS of both containers (EIP-7495) extend those of Heze by the appended fields.
Helpers
An index with BUILDER_INDEX_FLAG set names a builder, as for builder withdrawals (EIP-7732). A validator or builder has registered keys if its stored leanXMSS key is not all-zero.
defis_pq_public_key_in_use(state:BeaconState,xmss_public_key:XMSSPublicKey)->bool:"""
Return whether any validator or builder holds the leanXMSS key.
"""entries=list(state.post_quantum_key_material)+list(state.builder_post_quantum_key_material)returnany(entry.xmss_public_key==xmss_public_keyforentryinentries)
Clients can maintain a map from registered keys to indices, as they do for BLS public keys, so that this check does not scan the registries. Such a map removes a key when it is replaced or when its builder index is reassigned.
defis_valid_pq_possession_proof(state:BeaconState,pubkey:BLSPubkey,xmss_public_key:XMSSPublicKey,proof:XMSSSignature)->bool:"""
Check that the holder of a leanXMSS key signed it for a BLS public key, at leaf index PQ_POSSESSION_XMSS_INDEX.
The domain uses the genesis fork version, so a proof stays valid across forks.
"""ifget_xmss_signature_index(proof)!=PQ_POSSESSION_XMSS_INDEX:returnFalsepossession=PQKeyPossession(pubkey=pubkey,xmss_public_key=xmss_public_key)domain=compute_domain(DOMAIN_PQ_KEY_POSSESSION,GENESIS_FORK_VERSION,state.genesis_validators_root)returnxmss.Verify(xmss_public_key,compute_signing_root(possession,domain),proof)
Modified add_validator_to_registry
add_validator_to_registry appends an unset entry to post_quantum_key_material and post_quantum_key_change_epochs, preserving the invariant len(state.post_quantum_key_material) == len(state.post_quantum_key_change_epochs) == len(state.validators):
defadd_validator_to_registry(state:BeaconState,pubkey:BLSPubkey,withdrawal_credentials:Bytes32,amount:Gwei)->None:index=get_index_for_new_validator(state)validator=get_validator_from_deposit(pubkey,withdrawal_credentials,amount)set_or_append_list(state.validators,index,validator)set_or_append_list(state.balances,index,amount)set_or_append_list(state.previous_epoch_participation,index,ParticipationFlags(0b0000_0000))set_or_append_list(state.current_epoch_participation,index,ParticipationFlags(0b0000_0000))set_or_append_list(state.inactivity_scores,index,Uint64(0))set_or_append_list(state.post_quantum_key_material,index,PQKeyMaterial())# [New in this EIP]
set_or_append_list(state.post_quantum_key_change_epochs,index,FAR_FUTURE_EPOCH)# [New in this EIP]
PQKeyMaterial() is the all-zero entry.
Modified add_builder_to_registry
add_builder_to_registry writes an unset entry to builder_post_quantum_key_material and builder_post_quantum_key_change_epochs at the builder’s index, including a reused one, preserving the invariant len(state.builder_post_quantum_key_material) == len(state.builder_post_quantum_key_change_epochs) == len(state.builders). A builder’s deposit_epoch is set here and never modified afterwards, which the never-registered marker relies on:
defadd_builder_to_registry(state:BeaconState,pubkey:BLSPubkey,version:Uint8,execution_address:ExecutionAddress,amount:Gwei,slot:Slot,)->None:index=get_index_for_new_builder(state)builder=Builder(pubkey=pubkey,version=version,execution_address=execution_address,balance=amount,deposit_epoch=compute_epoch_at_slot(slot),withdrawable_epoch=FAR_FUTURE_EPOCH,)set_or_append_list(state.builders,index,builder)set_or_append_list(state.builder_post_quantum_key_material,index,PQKeyMaterial())# [New in this EIP]
set_or_append_list(state.builder_post_quantum_key_change_epochs,index,builder.deposit_epoch)# [New in this EIP]
Block processing
Modified process_operations
defprocess_operations(state:BeaconState,body:BeaconBlockBody,parent_slot:Slot)->None:# ... existing length checks ...
assertlen(body.pq_key_registrations)<=MAX_PQ_KEY_REGISTRATIONS# [New in this EIP]
# ... existing operation processing ...
for_ops(body.pq_key_registrations,process_pq_key_registration)# [New in this EIP]
New process_pq_key_registration
defprocess_pq_key_registration(state:BeaconState,signed_registration:SignedPQKeyRegistration)->None:registration=signed_registration.messagekey_material=registration.key_materialifis_builder_index(registration.index):builder_index=convert_validator_index_to_builder_index(registration.index)assertbuilder_index<len(state.builders)# Builders register once active, so after their deposit epoch is finalized
assertis_active_builder(state,builder_index)# Builders make no selection proofs
assertkey_material.vrf_root==VRFRoot()pubkey=state.builders[builder_index].pubkeystored_key=state.builder_post_quantum_key_material[builder_index].xmss_public_keylast_change_epoch=state.builder_post_quantum_key_change_epochs[builder_index]else:validator_index=registration.indexassertvalidator_index<len(state.validators)assertstate.validators[validator_index].exit_epoch==FAR_FUTURE_EPOCHpubkey=state.validators[validator_index].pubkeystored_key=state.post_quantum_key_material[validator_index].xmss_public_keylast_change_epoch=state.post_quantum_key_change_epochs[validator_index]# Replay protection and rate limit
assertregistration.previous_change_epoch==last_change_epochifstored_key!=XMSSPublicKey():assertget_current_epoch(state)>=last_change_epoch+MIN_PQ_KEY_CHANGE_DELAY# Authorization by the registrant's BLS signing key
domain=compute_domain(DOMAIN_PQ_KEY_REGISTRATION,GENESIS_FORK_VERSION,state.genesis_validators_root)assertbls.Verify(pubkey,compute_signing_root(registration,domain),signed_registration.signature)# A new leanXMSS key, whose possession is proven
assertkey_material.xmss_public_key!=XMSSPublicKey()assertnotis_pq_public_key_in_use(state,key_material.xmss_public_key)assertis_valid_pq_possession_proof(state,pubkey,key_material.xmss_public_key,signed_registration.possession)ifis_builder_index(registration.index):state.builder_post_quantum_key_material[builder_index]=key_materialstate.builder_post_quantum_key_change_epochs[builder_index]=get_current_epoch(state)else:state.post_quantum_key_material[validator_index]=key_materialstate.post_quantum_key_change_epochs[validator_index]=get_current_epoch(state)
Anyone can submit a registration. Only its BLS signature authorizes it.
Registrations are processed last: after the block’s other operations and after block.body.parent_execution_requests, which process_parent_execution_payload applies before process_operations. A registration for a registrant whose exit is initiated earlier in the block therefore invalidates the block. Block producers check registrations against the state after the block’s other operations.
A builder deposit request in parent_execution_requests can reuse a builder index, so process_pq_key_registration may read a different builder public key than the state before the block holds. A registration for that index in the same block is invalid either way, since the new builder’s deposit epoch is the current epoch and the builder is not active. Clients that verify a block’s signatures in a batch against the state before the block therefore reach the same result.
Fork transition
At the fork epoch, the upgrade function initializes one unset entry per validator and per builder:
defupgrade_to_<fork>(pre:heze.BeaconState)->BeaconState:post=BeaconState(# ... existing fields carried over from `pre` ...
# [New in this EIP]
post_quantum_key_material=ValidatorPQKeyMaterial(data=[PQKeyMaterial()for_inpre.validators]),# [New in this EIP]
post_quantum_key_change_epochs=ValidatorPQKeyChangeEpochs(data=[FAR_FUTURE_EPOCHfor_inpre.validators]),# [New in this EIP]
builder_post_quantum_key_material=BuilderPQKeyMaterial(data=[PQKeyMaterial()for_inpre.builders]),# [New in this EIP]
builder_post_quantum_key_change_epochs=BuilderPQKeyChangeEpochs(data=[builder.deposit_epochforbuilderinpre.builders]),)returnpost
Gossip
A new global topic pq_key_registration carries SignedPQKeyRegistration messages. The following validations MUST pass before forwarding a signed_registration on the network, where registration = signed_registration.message and the head state is the node’s state at its current head. They are listed in order, cheapest first.
[IGNORE] The current epoch, allowing for MAXIMUM_GOSSIP_CLOCK_DISPARITY, is at or after the epoch of the fork that activates this EIP.
[IGNORE] The head state is at or after the fork that activates this EIP, so that it has the registry lists of this EIP.
[IGNORE]registration.index is a validator index, or a builder index with BUILDER_INDEX_FLAG set, that exists in the head state. A registrant whose deposit was processed recently may not yet be in every node’s head state, so this is not a REJECT.
[REJECT] The registrant has not initiated an exit in the head state: a validator’s exit_epoch, or a builder’s withdrawable_epoch, is FAR_FUTURE_EPOCH.
[IGNORE] If registration.index is a builder index, the builder’s deposit_epoch is before the epoch of the head state’s finalized checkpoint, so that the builder is active. Finality can differ between nodes, so this is not a REJECT.
[REJECT] If registration.index is a builder index, registration.key_material.vrf_root is zero.
[IGNORE]signed_registration is the first valid registration received for the pair (registration.index, registration.previous_change_epoch).
[IGNORE]registration.previous_change_epoch is the stored change epoch of registration.index in the head state. A node whose head has not yet seen the last change, or has already seen a later one, holds another stored epoch, so this is not a REJECT.
[IGNORE] If the stored leanXMSS key of registration.index in the head state is not all-zero, the current epoch, allowing for MAXIMUM_GOSSIP_CLOCK_DISPARITY, is at least registration.previous_change_epoch + MIN_PQ_KEY_CHANGE_DELAY.
[REJECT] The leanXMSS key in registration.key_material is not all-zero.
[IGNORE] The leanXMSS key in registration.key_material is not registered for any index in the head state. The holder of a key can differ between heads, so this is not a REJECT.
[REJECT]signed_registration.signature is a valid BLS signature of registration by the registrant’s public key.
[REJECT]signed_registration.possession is a valid proof of possession of the leanXMSS key for the registrant’s BLS public key.
A registration that was valid when first received can become invalid before it is included, for example when the same leanXMSS key is registered for another index first. The stored change epoch then does not change, so the first-seen record would block every later registration for the pair. Nodes MAY clear the record for a pair once its first-seen registration is no longer valid against the finalized state.
While the first-seen registration for a pair stays valid but is not included, a different registration for the same pair, for example one that corrects a faulty key, is ignored. A registrant that broadcasts a faulty key replaces it once that registration is included and MIN_PQ_KEY_CHANGE_DELAY epochs have passed.
verify_block_body_operation_limits, which the beacon_block topic applies, gains one check:
defverify_block_body_operation_limits(body:BeaconBlockBody)->None:# ... existing checks ...
# [REJECT] The PQ key registration count is within the limit
iflen(body.pq_key_registrations)>MAX_PQ_KEY_REGISTRATIONS:# [New in this EIP]
raiseGossipReject("too many pq key registrations")
Key roles
A validator’s leanXMSS key is intended to sign every validator message once duties switch to it, and a builder’s to sign its execution payload bids and envelopes.
A signer MUST NOT sign twice at the same leaf index of a key, and chooses how it allocates leaf indices to messages. At leaf index PQ_POSSESSION_XMSS_INDEX, a signer MUST NOT sign anything other than the PQKeyPossession of its key for its registrant’s BLS public key. A signer MUST use each leanXMSS key in at most one registration, for one registrant, and MUST NOT register a key that it has registered before.
The leanXMSS seed and the VRF leaf secrets SHOULD NOT be derived from the registrant’s BLS key. Keystores and signer state for leanXMSS keys SHOULD follow EIP-8310, which extends ERC-2335.
The fork that uses the VRF root defines how its inputs map to indices.
Rationale
Registering before switching
Dormant keys cannot cause a missed attestation or a slashing, so the registry can ship before the aggregation and networking changes are ready. For the same reason, a change of keys replaces stored bytes and affects no duty.
One signing key and one VRF root
A stateful scheme such as leanXMSS signs at most once per leaf index, and several duties produce more than one message per slot. A second, stateless scheme would avoid leaf management at the cost of a second key and verifier and larger signatures. This EIP registers one leanXMSS key for all messages instead: since the signature carries its leaf index, new duties need no new key.
Selection proofs, such as aggregation and sync committee selection proofs, hash unique BLS signatures. leanXMSS signatures are not unique and could be ground, so the VRF root fixes one output per index at registration, before the validator knows its inputs. The root is not checked at registration, which keeps openings out of the registration, so validator clients check their tree before registering it. Builders make no selection proofs, so they register no VRF root.
Leaf index 0
Leaf index PQ_POSSESSION_XMSS_INDEX (0) is reserved for the proof of possession, so it is never used for a duty, whichever allocation the signer uses. A signer allocates leaf indices either freely, taking the next unused index and recording it in its state, or by a fixed per-slot allocation that gives each slot a range of indices. Under a per-slot allocation that starts at genesis, for example, leaf 0 belongs to the genesis slot, which is never a duty slot after this fork.
Proof of possession
The proof detects faulty key generation at registration rather than at the switch. With key uniqueness, it also prevents copying and squatting (see Key copying and squatting).
The proof signs PQKeyPossession rather than the whole registration, and the BLS signature covers the rest. It names the registrant by BLS public key rather than index: the public key never changes, and it is known before the deposit is processed, so the proof can be produced together with the keystore.
Uniqueness covers the registrant’s own key, so a change always carries a new leanXMSS key, even to change only the VRF root. This is cheap while the keys are dormant. Consensus does not record earlier keys, so returning to an earlier key or taking a released one is valid on chain, and the signer rules forbid both (see Leaf reuse).
The proof is 2404 of the 2580 bytes of a registration. It is kept on chain so that every registered key is known to work at the switch.
Authorization by the signing key
The operator who holds the BLS signing key also generates and holds the post-quantum key, so it registers each of its validators without a transaction from the stake owner, whatever the validator’s withdrawal credentials. The authorization is a consensus-layer operation only, as for BLSToExecutionChange, so this EIP needs no execution-layer change.
Allowing changes lets a registrant recover from a lost seed, lost signer state, or a faulty key without exiting. The cost is that an adversary able to forge BLS signatures can replace any registrant’s keys, not only those of unregistered ones (see Quantum adversaries before the switch). A register-only rule would keep keys registered before a break authentic, at the cost of an exit for every lost or faulty key.
The two can be combined over time. While a CRQC is believed distant, changes allow recovery. If one appears to be near, a fork can stop changes, or the operation altogether, and keys registered before that fork become the recovery anchor. This protects against a break that is anticipated, not against one that happens before the fork that stops changes.
Builders
Builders sign execution payload bids and envelopes with BLS keys from their own registry (EIP-7732), so they need post-quantum keys too. Registering them now gives them keys authorized while BLS is believed secure, without an exit and re-entry at the switch. They use the same operation, naming the builder by its index with BUILDER_INDEX_FLAG set, as builder withdrawals do.
Builder indices are reused once a builder has withdrawn, so FAR_FUTURE_EPOCH cannot mark a builder that has never registered: a first registration signed for an earlier builder at the same index would name it too. A builder’s entry instead starts at its deposit epoch. A registration requires an active builder, whose deposit epoch is finalized, so it always moves the stored epoch (see Builder index reuse).
Changes and replay protection
One stored epoch per index, named by each registration and set by each change, provides both replay protection and the rate limit. A per-index counter would prevent replays but not limit changes.
FAR_FUTURE_EPOCH marks a validator that has never registered, as it marks unset epochs elsewhere in the consensus specifications, and a builder’s deposit epoch plays that role for builders. A registration stores the current epoch, and with a delay of at least one epoch every change is applied after the epoch it names, so every applied registration changes the stored epoch and none applies twice. The delay check runs only for an entry that holds a key, whose stored epoch is that of its last change, so it never adds to FAR_FUTURE_EPOCH. A registration writes a nonzero key and the current epoch together, and onboarding writes the all-zero key and the marker together, so an entry holds a key exactly when its stored epoch is not its marker.
MIN_PQ_KEY_CHANGE_DELAY bounds each index to one change per 256 epochs, about 27 hours at 12-second slots. The delay is counted in epochs, so it lasts less time if slots become shorter. Changes and first registrations share MAX_PQ_KEY_REGISTRATIONS, and a registration that changes keys again within the delay wastes no block space, since it is invalid.
Interface instead of construction
This EIP specifies only the interface of the primitives and the properties it relies on, as EIP-4844 does for KZG. The registry does not depend on how the primitives are constructed, so their specification and test vectors can live with the other consensus cryptography.
Registry entries carry no version: this EIP accepts one set of primitives. A fork that changes the primitives before the switch defines how existing entries are handled, and introduces new DomainType constants for registrations and proofs of possession, so that registrations signed for the earlier primitives cannot be applied. Entries registered before that fork can then be told apart only by their change epoch.
Lists parallel to the validator and builder registries
The keys are stored in lists parallel to validators and builders. The Validator and Builder containers and their generalized indices do not change, so light clients and proofs against them are not affected until the switch.
Registration outside the deposit contract
The deposit contract’s event layout is fixed, and its events are converted into fixed-size DepositRequest records (EIP-6110). Neither these nor builder deposit requests have a field for post-quantum keys. Existing validators and builders also need a registration path that does not involve a deposit. A new validator registers through the same path once its deposit has been processed and it has an index, which can be before its activation. A new builder registers once it is active. Setting keys at deposit time requires a new deposit format, which is part of the fork that retires BLS deposits.
Per-block limits
MAX_PQ_KEY_REGISTRATIONS = 8 bounds the extra block size at about 20 KiB, and the verification cost at eight BLS verifications and eight leanXMSS verifications per block. Gossip rejects a block that exceeds it before the block is processed. At that rate a million validators register in 125,000 full blocks, about 17 days at 12-second slots, and less with shorter slots.
Backwards Compatibility
This EIP requires a hard fork of the consensus layer. It changes no existing duty: every message is still authenticated with BLS, and a registrant that never registers keys is unaffected by this fork.
Test Cases
leanXMSS test vectors for key generation, signing, and verification are in the tests/cryptography/xmss directory of the ethereum/EthLib repository at commit 6390c73ef040311e807b9047d99dca6b1be1c42f. The consensus test vectors cover at least:
A registration for an unregistered validator, and a change of its keys once MIN_PQ_KEY_CHANGE_DELAY epochs have passed, including a change that keeps the VRF root.
Rejection of a change before MIN_PQ_KEY_CHANGE_DELAY epochs have passed, and of a replayed registration whose previous_change_epoch no longer matches.
Rejection of a registration for a validator that has initiated an exit, and for a validator whose exit is initiated earlier in the same block by a proposer slashing, an attester slashing, a voluntary exit, a withdrawal request, or a consolidation request in parent_execution_requests.
A registration for an active builder with a zero VRF root, and a later change of its keys.
Rejection of a builder registration with a nonzero VRF root, for a builder that is not yet active, for a builder that has initiated an exit, including by a builder exit request in parent_execution_requests, and for a builder index outside the builder registry.
Rejection of a key already registered for another index or for the registrant itself, of an invalid proof of possession, and of a proof of possession at a leaf index other than PQ_POSSESSION_XMSS_INDEX.
Rejection of the all-zero leanXMSS key in a state where every validator and builder holds a key.
Rejection of a block with two registrations of the same leanXMSS key for two validators, or for a validator and a builder, where the second registration is invalid.
Gossip rejection of a block with more than MAX_PQ_KEY_REGISTRATIONS registrations.
A registration, valid on chain, of a key that another validator held and released through a change, including by a builder that shares that validator’s BLS public key with the same proof bytes.
After a builder index is reassigned, rejection of a registration signed for the earlier builder at that index, including one signed by the same BLS key after it re-enters at that index. A deposit for a public key still in the builder registry is a top-up, so that key returns to the index only after another builder has held it in between.
The fork upgrade, which sets each builder’s change epoch to its deposit epoch, add_builder_to_registry on a reused index, and the length invariants stated for add_validator_to_registry and add_builder_to_registry.
Security Considerations
Quantum adversaries before the switch
A registration is only as authentic as the BLS signature that authorizes it. Once a CRQC exists, an adversary can register or replace the keys of any registrant, at most once every MIN_PQ_KEY_CHANGE_DELAY epochs per index. The switch therefore has to happen while BLS is believed secure, or a fork has to stop key changes before a break, so that the keys registered until then can be trusted (see Authorization by the signing key). Once duties use the registered keys, a BLS-authorized change would let such an adversary replace keys in use, so the fork that switches duties to them has to stop BLS-authorized changes.
Primitive security
The registry is only as secure as the primitives it registers. The leanXMSS specification, over Keccak-256, proves 127 bits of classical strong unforgeability in the random oracle model, and its parameters target 64 bits of quantum security, NIST level 1. That target is not a proved bound. A VRF output is unique unless a hash collision is found, which for 32-byte nodes costs about 2**128 hash evaluations classically and about 2**85 quantumly with the Brassard–Høyer–Tapp algorithm.
Leaf reuse
Each leaf index signs at most once, whichever allocation the signer uses: slashing protection enforces it under a fixed per-slot allocation, and the signer’s state under free allocation. Each key is used in one registration, so leaf PQ_POSSESSION_XMSS_INDEX signs one proof, and a registration whose block is orphaned is included again with the same bytes. Consensus does not track released keys and accepts one again. Returning to an earlier key needs a second proof or the stored first one. Taking a key that another registrant released needs a proof of a different message at leaf PQ_POSSESSION_XMSS_INDEX, unless the two registrants share a BLS public key, in which case the released key’s stored proof applies. The signer rules forbid all of these.
Key copying and squatting
The registry rejects a key that any registrant has already registered. Without that rule, a validator could register another’s key and claim its signatures as its own participation, since the signed messages do not name the validator. The proof of possession signs the registrant’s BLS public key, so a copied key, even one published but not yet registered, cannot be registered for another registrant. A validator and a builder may share a BLS public key, since they enter through different deposit paths. Their proofs of possession for a key are then the same message, so uniqueness keeps one leanXMSS key from serving both at once, and only the signer rules keep it from moving between them. This binding relies on BLS public keys never changing: if a later fork made them mutable, a key bound to an outdated public key would need a second proof at leaf PQ_POSSESSION_XMSS_INDEX, which is a leaf reuse.
Readiness is not registration
A registered key proves that its holder could sign once, at registration time. It does not prove that the validator will run post-quantum software, keep its seed, or keep its signer state consistent at the switch. The effective balance of active validators with registered keys is therefore an upper bound on readiness, so the switch needs a margin above the two-thirds finality threshold.
VRF tree
The registry accepts a VRF root that another validator has registered. A validator that copies a root cannot open it before its owner does, but it has the same outputs and can replay the owner’s openings once they are published. The fork that uses the tree has to account for this.
Builder index reuse
A builder index is reassigned once its builder has exited and withdrawn. add_builder_to_registry then resets the entry, releasing the earlier builder’s key and starting the change epoch at the new builder’s deposit epoch, which never changes afterwards. The earlier builder’s last change epoch is at most its exit epoch, and the index can be reassigned only MIN_BUILDER_WITHDRAWABILITY_DELAY epochs after that exit. With that delay at least 1, every registration signed for an earlier builder at that index names an epoch before the new builder’s deposit epoch, so none of them applies. This includes the same BLS key re-entering at that index, which can only happen after another builder has held it in between, since a deposit for a public key still in the builder registry is a top-up.
Denial of service and state growth
Each registration costs one BLS and one leanXMSS verification, and the registry grows with the validator and builder sets:
Blocks. At most MAX_PQ_KEY_REGISTRATIONS registrations per block; see Per-block limits.
Gossip. Registrations are first-seen per index and previous change epoch, and changes are rate-limited per index (see Changes and replay protection).
State. Each validator and builder index ever assigned, exited ones included, adds 72 bytes: 64 bytes of keys and an 8-byte change epoch, about 72 MB per million indices.