---
eip: 8321
title: Hash-Chain RANDAO
description: Replace the BLS-signature RANDAO reveal with a post-quantum hash-chain commit-reveal scheme
author: Kevaundray Wedderburn (@kevaundray), Benedikt Wagner (@benedikt-wagner), Tom Wambsgans (@TomWambsgans), Justin Drake (@JustinDrake), Thomas Coratger (@tcoratger)
discussions-to: https://ethereum-magicians.org/t/eip-8321-hash-chain-randao/28942
status: Draft
type: Standards Track
category: Core
created: 2026-07-05
requires: 7916
---

## Abstract

Replace the BLS-signature-based Random Decentralized Autonomous Organization (RANDAO) reveal with a hash-chain commit-reveal scheme. RANDAO's resistance to grinding currently relies on BLS signatures being *unique*, so that a proposer cannot bias its contribution. Since BLS relies on pre-quantum hardness assumptions, a quantum computer can recover a validator's key and predict its future reveals.

A hash chain relies only on standard hash-function security: collision resistance to prevent grinding (the property that replaces BLS's uniqueness) and preimage resistance for unpredictability. Both are believed to hold against quantum attack, and this removes the dependency on the signature scheme entirely. Each validator commits to the tip of a generated hash chain. When proposing a block, the validator reveals the preimage of its currently stored commitment; the protocol verifies that the revealed value is indeed the preimage to the current stored commitment for that validator, folds the preimage into the RANDAO accumulator, and stores the preimage as the validator's new commitment.

A commitment is registered once, via a new per-block-capped beacon operation (similar to `BLSToExecutionChange`); it cannot be updated in place, so a validator that ever needs a new chain exits and re-enters. Validators that have not yet registered a commitment continue to use the legacy BLS reveal; this is a transitional path intended for removal in a later fork.

We note the simplicity: the protocol holds only the current commitment (32 bytes) and walks one link back per proposal for that validator.

## Motivation

The primary driver is post-quantum readiness.

Today's RANDAO contribution is a BLS signature over the epoch number. Its security rests on the *uniqueness* of BLS signatures: given the message and public key, exactly one valid signature exists, so a proposer cannot grind their own contribution to bias the randomness.

BLS is not post-quantum safe, so a cryptographically relevant quantum computer (CRQC) can recover a validator's secret key from its public key. Uniqueness still holds, but the attacker can compute the validator's reveals itself, predicting the chain's future randomness, and thus the proposer schedule, far in advance.

Prediction also amplifies bias, beyond merely leaking the schedule. The standard reveal-or-withhold attack requires the attacker to control `k` *consecutive* proposer slots at the tail of an epoch to choose among `2**k` candidate mixes, because it cannot evaluate a candidate without knowing the contributions that land after its own. An attacker that can predict every honest contribution evaluates each candidate outright, so (assuming honest proposers always reveal) any `k` of its slots in the mixing period (the span of slots whose reveals feed the target seed) work; they need not be consecutive or at the end of the epoch.

Most uses of signatures, such as block proposals and attestations, can move to any secure post-quantum scheme. RANDAO, however, additionally depends on signature *uniqueness*, and most post-quantum schemes do not provide it, allowing one to grind for a favorable RANDAO contribution. The hash-based signature scheme currently planned for the consensus layer (a generalized eXtended Merkle Signature Scheme (XMSS) ([RFC 8391](https://www.rfc-editor.org/rfc/rfc8391)), commonly referred to as the lean signature scheme) is grindable in exactly this way, since it includes a *salt* component.

A hash chain sidesteps both problems in a simple way. There is no signature, it is just chaining hashes together. Grinding would require building a chain around a hash collision: if a proposer could find two values that hash to the same word, it could place that word in its chain and later choose which of the two preimages to reveal, biasing its contribution. This is exactly the freedom BLS's uniqueness denies, and collision resistance denies it here. Preimage resistance separately keeps each reveal unpredictable to others until it is published. Both properties are believed to hold even against a quantum attacker, with reduced but adequate security.

The commit-reveal structure also preserves RANDAO's existing security model, since the whole chain is fixed at commitment time, long before the validator knows its proposal slots or the mixes it might want to bias, so the proposer's only remaining lever is the same as the one it has today: reveal, or withhold and forfeit the block.

Note: This EIP does not make beacon chain randomness post-quantum secure end to end; block signatures and the registration operation's signature still use BLS. It incrementally introduces a post-quantum version of RANDAO, so that a later post-quantum fork which changes the signature-scheme and sets the initial hash-chain commitment at deposit time can complete the transition.

## 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).

These changes are applied to the consensus specifications (`ethereum/consensus-specs`) at a fork to be scheduled; functions and constants not defined here retain their meaning from those specifications.

### Cryptographic Functions

`blake3(data: bytes) -> Bytes32` is the BLAKE3 hash function (version 1) in its default unkeyed hash mode, with no derive-key context, restricted to its default 32-byte output. All hashing introduced by this EIP uses `blake3` (see Rationale); the consensus specifications' `hash` helper continues to serve the legacy reveal path.

### Constants

| Name | Value | Description |
| - | - | - |
| `DOMAIN_RANDAO_COMMITMENT_REGISTRATION` | `DomainType('0x0F000000')` | Domain for signed commitment registrations |
| `HASH_CHAIN_RANDAO_DST` | `b'HASH_CHAIN_RANDAO'` | Domain-separation tag prefixed to each hash-chain link |

### Preset

| Name | Value | Description |
| - | - | - |
| `COMMITMENT_REGISTRATION_DELAY` | `Epoch(3)` | Epochs before a new commitment becomes active; MUST be at least `MIN_SEED_LOOKAHEAD + 2` (see Rationale) |
| `MAX_RANDAO_COMMITMENT_REGISTRATIONS` | `uint64(128)` | Maximum commitment registration operations per block |

### Hash Chain Construction (Off-Chain)

A validator generates a uniformly random 32-byte chain secret `c_0` and computes a chain of length `n`; every chain value `c_i` is a `Bytes32`, matching the BLAKE3 output width:

```text
c_i = blake3(HASH_CHAIN_RANDAO_DST + c_{i-1})    for i in 1..n
```

where `blake3` is defined in the Cryptographic Functions section above. `HASH_CHAIN_RANDAO_DST` is the fixed byte-string domain-separation prefix defined in the Constants section above, so hash-chain links cannot collide with hashes used elsewhere in the protocol. It carries no per-link index, so a validator still needs to store only chain values, not their positions.

Nothing in the construction is specific to BLAKE3; any collision- and preimage-resistant hash with a 32-byte output serves (see Rationale for why BLAKE3 over the consensus specifications' `hash` helper). Note that a commitment binds the hash function it was generated with, so a later fork that migrates to a different hash either keeps verifying previously registered chains under BLAKE3 or has validators re-register under the new hash.

Chain values MUST NOT be the zero word: `process_randao` rejects a zero reveal (the zero word marks an unregistered validator), and a zero commitment cannot be registered. The seed `c_0` is chosen non-zero, and every other `c_i` is a BLAKE3 output, so a zero link occurs only with negligible probability (about `n / 2**256`). Even so, a validator MUST check that no `c_i` is the zero word and regenerate the chain if any link is zero.

The validator publishes `c_n` as their commitment and stores the chain (or the chain secret plus periodic checkpoints if the chain is large). Reveals are consumed in reverse order: the first reveal is `c_{n-1}`, the next is `c_{n-2}`, and so on. Each revealed value becomes the new on-chain commitment, so the protocol requires no knowledge of `n` or of the validator's position in the chain.

Validators SHOULD choose `n` large enough that the chain outlasts the validator, since it cannot be extended in place. `n >= 2**16` (~65,000 links) is RECOMMENDED. The whole chain can be generated in milliseconds, and storing every link takes ~2 MB. This exact value is of course arbitrary: the protocol never learns or enforces `n`, and since generation and storage stay cheap, most operators lose nothing by choosing a larger `n`.

This bounded lifetime is the one structural difference from BLS, where a public key is a commitment that never expires, a hash chain instead lasts for its length. It is not a practical constraint though since we can generate a large enough chain that will last for centuries.

#### Chain Exhaustion

A commitment cannot be updated in place, so a chain cannot be extended once registered. The chain is exhausted only when its stored commitment reaches the secret seed `c_0`, whose preimage the validator does not hold. A validator whose chain runs out can no longer propose on the hash-chain path and must exit and re-enter as a new validator to obtain a fresh chain.

With the recommended `n` this never happens in practice. A validator proposes on the order of 100 times per year even in aggressive futures, so a chain of `2**16` links lasts for centuries; sizing `n` generously (it is cheap) makes exhaustion a non-issue. The same applies to a lost or mis-generated chain secret: there is no in-place recovery, so the remedy is exit and re-entry, and the chain secret SHOULD be guarded like the signing key (see Security Considerations).

### Containers

#### New Containers

```python
class RandaoCommitmentRegistration(Container):
    validator_index: ValidatorIndex
    commitment: Bytes32  # the hash-chain commitment to register
```

```python
class SignedRandaoCommitmentRegistration(Container):
    message: RandaoCommitmentRegistration
    signature: BLSSignature
```

```python
class PendingRandaoCommitment(Container):
    validator_index: ValidatorIndex
    commitment: Bytes32
    activation_epoch: Epoch
```

#### Modified Containers

`BeaconBlockBody` gains two fields and retains `randao_reveal` transitionally:

```python
class BeaconBlockBody(Container):
    randao_reveal: BLSSignature  # transitional; MUST be the G2 point at infinity once the proposer has an active commitment
    # ... existing fields ...
    hash_chain_reveal: Bytes32  # [New in this EIP] zero unless the proposer has an active commitment
    randao_commitment_registrations: List[SignedRandaoCommitmentRegistration, MAX_RANDAO_COMMITMENT_REGISTRATIONS]  # [New in this EIP]
```

The new fields are appended after all existing fields, following the convention since Capella of adding fields at the end.

`BeaconState` gains a commitment registry and a pending queue. `randao_commitments` is indexed by validator index, holding one entry per registry member; a zero entry means no commitment is registered and the legacy BLS reveal path applies. The `Validator` container is unchanged.

```python
class BeaconState(Container):
    # ... existing fields ...
    randao_commitments: List[Bytes32, VALIDATOR_REGISTRY_LIMIT]  # [New in this EIP]
    pending_randao_commitments: ProgressiveList[PendingRandaoCommitment]  # [New in this EIP]
```

The pending queue is a `ProgressiveList` ([EIP-7916](./eip-7916.md)): it is almost always near-empty (worst-case steady state is ~12,300 entries, see Security Considerations), so a progressive shape avoids both an arbitrary capacity constant and the hashing overhead of a large fixed-limit list. `randao_commitments` stays a fixed-limit `List` because it holds one entry per registry member and must track the length of the other per-validator lists, which share the `VALIDATOR_REGISTRY_LIMIT` bound.

### Block Processing

#### Modified `process_randao`

```python
def process_randao(state: BeaconState, body: BeaconBlockBody) -> None:
    epoch = get_current_epoch(state)
    proposer_index = get_beacon_proposer_index(state)
    proposer = state.validators[proposer_index]
    if state.randao_commitments[proposer_index] != Bytes32():
        # Hash chain reveal [New in this EIP]
        assert body.hash_chain_reveal != Bytes32()
        assert blake3(HASH_CHAIN_RANDAO_DST + body.hash_chain_reveal) == state.randao_commitments[proposer_index] # check proposer knows preimage
        assert body.randao_reveal == G2_POINT_AT_INFINITY # bls randao reveal should be empty
        mix = blake3(get_randao_mix(state, epoch) + body.hash_chain_reveal)
        state.randao_commitments[proposer_index] = body.hash_chain_reveal
    else:
        # Legacy BLS reveal
        assert body.hash_chain_reveal == Bytes32() # hash-chain reveal should be empty
        signing_root = compute_signing_root(epoch, get_domain(state, DOMAIN_RANDAO))
        assert bls.Verify(proposer.pubkey, signing_root, body.randao_reveal)
        mix = xor(get_randao_mix(state, epoch), hash(body.randao_reveal))
    state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mix
```

Note the structure of the hash-chain path:

- Verification checks the proposer's chain step (`blake3(HASH_CHAIN_RANDAO_DST + reveal) == commitment`).
- The accumulator folds in the raw reveal with `mix = blake3(mix + reveal)`.
- `G2_POINT_AT_INFINITY` is the existing BLS point-at-infinity signature constant already defined in the consensus specs.

The hash accumulator has no efficiently computable inverse, so a validator that copies another's commitment cannot cancel the victim's contribution: re-injecting the same revealed value produces a fresh, unrelated mix rather than undoing it. This is why the hash-chain path can fold in the raw reveal directly, with no assumption on the mixed-in value being unique per validator. The legacy BLS path keeps its existing `xor` accumulator; the two coexist only until the BLS path is sunset.

The non-zero assert protects the sentinel: a zero entry in `randao_commitments` means "unregistered", and the reveal is stored as the next commitment. Without the guard, a validator that committed to the zero word as a chain value would, upon revealing it, silently store the sentinel and flip onto the legacy branch, while its client, still believing itself registered, produced invalid blocks indefinitely. With the guard, the zero-revealing block is itself invalid, so a validator that committed the zero word simply cannot propose (hence the rule above that no chain value may be the zero word). So "zero means unregistered" is an enforced invariant.

#### Modified `process_operations`

`process_operations` gains a loop over the new operation, appended after the existing operations so that existing processing is unchanged:

```python
def process_operations(state: BeaconState, body: BeaconBlockBody) -> None:
    # ... existing operation processing (proposer slashings, attester slashings,
    #     attestations, deposits, voluntary exits, bls_to_execution_changes) ...
    for_ops(body.randao_commitment_registrations, process_randao_commitment_registration)  # [New in this EIP]
```

#### New `process_randao_commitment_registration`

```python
def process_randao_commitment_registration(state: BeaconState, signed_registration: SignedRandaoCommitmentRegistration) -> None:
    registration = signed_registration.message
    assert registration.validator_index < len(state.validators)
    assert registration.commitment != Bytes32()
    # Register-only: valid only while the validator is unregistered (stored commitment is zero)
    assert state.randao_commitments[registration.validator_index] == Bytes32()
    validator = state.validators[registration.validator_index]
    domain = compute_domain(
        DOMAIN_RANDAO_COMMITMENT_REGISTRATION,
        genesis_validators_root=state.genesis_validators_root,
    )
    signing_root = compute_signing_root(registration, domain)
    assert bls.Verify(validator.pubkey, signing_root, signed_registration.signature)
    queue_randao_commitment(state, registration.validator_index, registration.commitment)
```

Called from `process_operations` for each element of `body.randao_commitment_registrations`. This is the one-time registration path: it moves a validator from the legacy BLS reveal onto its hash chain, and works regardless of proposal schedule. It is valid only while the validator is unregistered (its stored commitment is zero). The signing domain is computed against the genesis fork version (similar to `BLSToExecutionChange`) so that messages remain valid across forks.

Registration is single-use by construction, covering two windows with two mechanisms. Once a registration has **activated**, `randao_commitments[validator_index]` is non-zero, so any replay fails the unregistered check in this handler. While it is still **pending** (queued but not yet activated), the stored entry is still zero, but the one-pending rule in `queue_randao_commitment` rejects a second submission. Together these leave no gap from inclusion onward. This mirrors the replay-safety of `BLSToExecutionChange`, whose handler likewise asserts the credential has not already been changed.

#### New `queue_randao_commitment`

At most one registration per validator may be pending at any time; a block containing a registration for a validator with an in-flight one is invalid.

```python
def queue_randao_commitment(state: BeaconState, index: ValidatorIndex, commitment: Bytes32) -> None:
    # Rejects a second registration for the same validator, both across blocks and
    # within a single block: the first op appends this entry, so a later op for the
    # same index fails this assert (mirrors how voluntary exits self-reject in-block).
    assert all(pending.validator_index != index for pending in state.pending_randao_commitments)
    state.pending_randao_commitments.append(PendingRandaoCommitment(
        validator_index=index,
        commitment=commitment,
        activation_epoch=get_current_epoch(state) + COMMITMENT_REGISTRATION_DELAY,
    ))
```

The resulting lifecycle, worked through to be more explicit:

- A registration included in epoch `N` is queued with activation epoch `N + COMMITMENT_REGISTRATION_DELAY` (`N + 3` with the preset value).
- The validator stays on the legacy BLS path throughout epochs `N` to `N + 2`; its proposals in that window are BLS reveals as before.
- At the epoch transition into `N + 3`, the pending entry is consumed and the hash chain becomes active; from then on the validator reveals from its chain.

Activation is a property of the canonical state, not of the validator's broadcast history. A validator MUST continue producing legacy BLS reveals until `randao_commitments[validator_index]` is non-zero in the state it proposes against, even if it has observed its registration included in some block: if that block is orphaned, the pending entry never enters the canonical queue. The message stays valid in that case (the stored commitment is still zero) and remains includable: it persists in operation pools, as with `bls_to_execution_change`.

### Epoch Processing

#### New `process_pending_randao_commitments`

Called from `process_epoch`, immediately after `process_registry_updates`. Pending commitments are applied in queue order once their activation epoch is reached. The one-pending-registration-per-validator rule enforced in `queue_randao_commitment` guarantees entries never conflict.

```python
def process_pending_randao_commitments(state: BeaconState) -> None:
    next_epoch = Epoch(get_current_epoch(state) + 1)
    remaining = []
    for pending in state.pending_randao_commitments:
        if pending.activation_epoch <= next_epoch:
            state.randao_commitments[pending.validator_index] = pending.commitment
        else:
            remaining.append(pending)
    state.pending_randao_commitments = ProgressiveList[PendingRandaoCommitment](remaining)
```

### Gossip

A new global gossip topic `randao_commitment_registration` carries `SignedRandaoCommitmentRegistration` messages. Because a validator registers at most once, first-seen-per-validator deduplication suffices, exactly as for `bls_to_execution_change`. The rules are evaluated in order, so the cheap checks and the seen check precede signature verification.

- **[REJECT]** `commitment` is zero, or `validator_index` is unknown.
- **[IGNORE]** a `SignedRandaoCommitmentRegistration` for `validator_index` has already been seen, or a pending registration for it already exists in the node's view of the state.
- **[REJECT]** the validator is already registered (a non-zero `randao_commitments` entry in the node's view of the head state).
- **[REJECT]** the signature is invalid.

### Fork Transition

At the fork epoch, the `upgrade_to_*` function initializes `randao_commitments` with one zero entry per registry member and `pending_randao_commitments` as empty:

```python
def upgrade_to_<fork>(pre: <PreForkState>) -> BeaconState:
    post = BeaconState(
        # ... existing fields carried over from `pre` ...
        randao_commitments=[Bytes32() for _ in range(len(pre.validators))],  # [New in this EIP]
        pending_randao_commitments=[],  # [New in this EIP]
    )
    return post
```

This establishes the invariant that `len(state.randao_commitments) == len(state.validators)`, which `process_randao` and `process_randao_commitment_registration` rely on when indexing by validator index. To preserve the invariant for validators onboarded after the fork, `add_validator_to_registry` is modified to append a zero entry alongside the other per-validator lists:

```python
def add_validator_to_registry(state: BeaconState,
                              pubkey: BLSPubkey,
                              withdrawal_credentials: Bytes32,
                              amount: uint64) -> 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.randao_commitments, index, Bytes32())  # [New in this EIP]
```

A validator added this way starts unregistered and uses the legacy path until it registers. All validators start on the legacy BLS reveal path and migrate by broadcasting a `SignedRandaoCommitmentRegistration`. At `MAX_RANDAO_COMMITMENT_REGISTRATIONS = 128` per block, the full current validator set (~1M) can register in under two days of full blocks; there is no deadline, and unregistered validators simply continue on the legacy path.

### Sunset of the Legacy Path

The BLS reveal branch in `process_randao`, and the `randao_reveal` field itself, are transitional and SHOULD be removed in a later fork after registration has saturated, naturally the fork that reworks deposits for post-quantum signatures, at which point initial hash chain commitments move into the validator onboarding flow and the `RandaoCommitmentRegistration` signature migrates to the post-quantum scheme.

Note: the `RandaoCommitmentRegistration` operation registers a validator's initial hash chain only. A validator that loses its chain secret must exit and re-enter. Once the post-quantum fork sets initial commitments at deposit time, the operation is no longer needed and can be deprecated alongside the BLS reveal path.

## Rationale

### Why a Hash Accumulator on the Hash-Chain Path

The mixed-in contribution must be unpredictable before the reveal: the mix must never absorb a value derivable from pre-reveal public state. This rules out folding in `blake3(HASH_CHAIN_RANDAO_DST + reveal)`, which for a hash chain is by definition the validator's stored commitment, already public on-chain. The raw reveal is the opposite: it is the secret preimage, unknown to everyone until the proposer publishes it, so folding it in is safe.

The complication is that hash-chain commitments carry no identity and are trivially copyable. A validator can register a value it did not generate by including a copy of another validator's current commitment, via a perfectly valid, freshly signed registration. Under a linear XOR accumulator this enables a cancellation attack.
A hash accumulator, `mix = blake3(mix + reveal)`, closes this at the root: the hash has no efficiently computable inverse, so re-injecting a copied reveal produces a fresh, unrelated mix rather than undoing anything.

Because the accumulator itself neutralizes copyability, the hash-chain path folds in the raw reveal with no per-validator personalization.

### Why BLAKE3 Instead of the Consensus `hash`

The scheme needs nothing beyond standard collision and preimage resistance; the choice of BLAKE3 is forward-looking rather than security-driven. BLAKE3 is fast in software, and recent results on proving it efficiently in binary-field proof systems make it the leading candidate hash for the post-quantum consensus layer.

### Why One Link per Block Proposed

The legacy reveal is a signature over the epoch, so two proposals by the same validator in one epoch reveal the same value. Consuming one link per block is simpler (no per-epoch bookkeeping), makes every block contribute fresh entropy, and costs nothing, since chains are cheap to generate at any reasonable length.

We also note that with consolidations, the probability of a validator proposing multiple times in the same epoch increases, effectively cancelling out their RANDAO contribution.

### Why the Protocol Does Not Track Chain Length

Storing only the current commitment and requiring a preimage per proposal makes the chain length a purely private, off-chain parameter. The protocol state cost is one 32-byte entry per validator regardless of chain length, and validators can size chains so that the chain outlasts the validator. The only reason to store the chain length would be to mandate a specific length for every validator, which seems unnecessarily restrictive.

### Why an Activation Delay on Registration, and Why `MIN_SEED_LOOKAHEAD + 2`

The scheme's grinding resistance comes entirely from the commitment predating the validator's knowledge of what it would want to bias. If a validator could register a chain just before proposing and it became active instantly, it could grind: generate many candidate chains, compute the mix each would produce at the upcoming slot, and register the most favourable one. We add a delay, sized so that the registrant cannot even know *whether* it will propose in the activation epoch at the time the registration is included.

Proposer duties for epoch `E` are drawn from the RANDAO mix as of the end of epoch `E - MIN_SEED_LOOKAHEAD - 1`, i.e. `E - 2` with the current consensus-specs preset `MIN_SEED_LOOKAHEAD = 1`. For a registration included in epoch `N`:

- Duties through epoch `N + 1` fixed at the end of `N - 1` or earlier: already known at inclusion.
- Duties for `N + 2` fix at the end of `N`, which is nearly over at inclusion: still grindable.
- Duties for `N + 3` fix at the end of `N + 1`: entirely after inclusion.

The earliest safe activation epoch is therefore `N + MIN_SEED_LOOKAHEAD + 2 = N + 3`, met exactly by `COMMITMENT_REGISTRATION_DELAY = 3`; the new commitment is first usable from the validator's first proposal in that epoch. The constants table states the bound symbolically so it survives a future change to the preset.

### Why at Most One Pending Registration per Validator

Without the one-pending rule, a validator's stored entry stays zero until activation, so multiple registrations for it would all pass the unregistered check and could be queued at once. That enables queue stuffing: the entire `MAX_RANDAO_COMMITMENT_REGISTRATIONS` budget could be filled with registrations for a single validator, costing the network 128 signature verifications and 128 queue entries for a single state effect, and crowding legitimate registrations out of the shared per-block budget.

The one-pending rule also keeps gossip sound: the "IGNORE if a pending registration exists" rule is only meaningful if a further registration for that validator is guaranteed redundant. With the rule, every included registration corresponds to a distinct validator making its one-time transition, and duplicates are invalid rather than merely wasteful.

### Why Registration Is One-Time, and Replay-Safe

A commitment can be registered but never updated in place. This is a deliberate simplification: because a chain can be sized to outlast the validator (see Chain Exhaustion), in-place rotation is never needed for exhaustion, and a lost chain secret is treated like a lost signing key, recovered by exiting and re-entering rather than by a protocol operation.

Replay-safety falls out of the unregistered check. A registration is valid only while `randao_commitments[validator_index]` is zero. Once it activates, the entry is non-zero, so any later replay of the message is invalid; and while the registration is merely pending, the one-pending rule blocks a second submission. This is the analogue of the idempotence predicate that makes `BLSToExecutionChange` replay-safe, whose handler asserts the credentials are still BLS-prefixed, which the first application falsifies.

### Why Keep a Legacy Fallback Instead of a Registration Deadline

There is no urgent need for validators to be fully post-quantum yet, so a hard deadline buys little; unregistered validators keep functioning on the BLS path in the meantime. There will also likely be a long tail of validators who never submit the message because they run custom software. We therefore defer the forced cutover to a fork that must touch these code paths anyway, the post-quantum switchover, which also lets us exercise the new code paths first.

## Backwards Compatibility

This EIP requires a scheduled consensus-layer hard fork. Within the fork, the change is backwards compatible from the validator's perspective: unregistered validators continue proposing exactly as today. Downstream consumers of `randao_mixes` (including the execution layer's [EIP-4399's](./eip-4399.md) `PREVRANDAO`) are unaffected; the mix remains a 32-byte accumulator updated once per block, only the provenance of contributions changes.

## Test Cases

State-transition test vectors to be provided in the consensus-specs test suite.

## Security Considerations

### Biasability Is Unchanged

The proposer's only degree of freedom is still withholding: reveal and propose, or withhold and forfeit the block plus its rewards. This is the same one-bit-per-proposer bias RANDAO has today, with the same economic cost. The validator freely choosing its own chain values (whereas a BLS reveal is uniquely determined by the key and epoch, leaving no freedom) does not add grinding power, because the entire chain is fixed before the validator knows its proposal slots or the co-contributions to any future mix.

### Registration Cannot Be Used Reactively

A validator that sees a proposal duty approaching cannot register a favourable chain to influence the outcome. Activation is derived from the inclusion epoch, the only event the protocol observes: registration activates `COMMITMENT_REGISTRATION_DELAY` epochs after inclusion, strictly after every epoch whose proposer-duty seed was fixed (or partially accumulated) at inclusion, so the chain is always fixed before the seed that draws any duty it could serve. Signing earlier than inclusion only means committing with less information.

Note: This argument assumes the registrant cannot predict the contributions that land between inclusion and the fixing of the target seed. That holds today (a BLS reveal is computable only by its key holder) and after migration (a hash-chain reveal is protected by preimage resistance), but an attacker that already holds a CRQC during the transition could predict every remaining legacy contribution and grind its one-time registration against them.

Timing games around inclusion add nothing: an unregistered validator stays on the BLS path until activation, and once registered it cannot re-register, so there is no second chain to time.

Nor can registrations be replayed by third parties: once a validator is registered its stored entry is non-zero, so any replay fails the unregistered check.

### Degenerate Chains

A validator may choose pathological values for its *own* chain, for example deriving the chain secret from a publicly known constant like the genesis hash, so that all its reveals are predictable in advance and contribute no effective entropy.

This cannot be exhaustively restricted, and we do not try to, because RANDAO's security does not assume that *every* contribution is honest. If this were the case, then a validator choosing to add nothing by withholding would also break the security; one honest contributor per mixing period suffices.

We do forbid the pathological case of using the zero word, since this would collide with the unregistered sentinel when revealed, so `process_randao` rejects zero reveals.

### Chain Loss and Theft

Losing the chain secret makes a validator unable to produce valid blocks on the hash-chain path, and there is no in-place recovery: a commitment cannot be re-registered. The remedy is to exit and re-enter as a new validator. Funds are unaffected (withdrawal does not depend on the chain), and the failure mode is missed proposals, not slashing risk.

Theft of the chain secret alone lets an attacker predict (not choose) the validator's future contributions. In effect this is equivalent to leaking the validator's entire list of future BLS-RANDAO reveals: the attacker learns every future contribution in advance, but can neither choose them nor sign anything else on the validator's behalf. It is therefore strictly less power than theft of the signing key today, which gives the attacker that same prediction plus the ability to sign the validator's blocks and attestations. Validator clients should treat the chain secret with the same custody standards as signing keys. As a safety measure, it should not be derived in a way that links it to the signing key: a hash chain, taken to its limit, eventually reveals its seed, whereas a secret key must never be revealed.

### Reveal Exposure in Orphaned Blocks

The legacy BLS reveal signs the epoch number, so a reveal exposed in a block that never makes the canonical chain is worthless outside that epoch. A hash-chain reveal has no such time binding: a reveal published in an *orphaned* block remains the validator's mandatory next contribution at whatever future slot it next proposes. An observer who collects reveals from orphaned blocks (or losing forks) accumulates known future co-contributions.

Because a chain cannot be retired in place, there is no cheap remediation, but none is needed: the exposure is harmless. A published-but-not-yet-consumed reveal only makes that one future contribution predictable rather than secret, which is no worse than the validator withholding it, and RANDAO tolerates predictable contributions (one honest contributor per mixing period suffices). A validator sufficiently concerned about a specific exposed chain can exit and re-enter, but this is unwarranted in practice.

### Erroneous Commitments

Preimage possession is unverifiable at registration: the protocol cannot distinguish a commitment whose chain the validator holds from a typo or the output of a buggy key-derivation.

Because registration is one-time, a bad commitment cannot be corrected in place: the validator can never propose on the hash-chain path, and the only remedy is to exit and re-enter. This makes an erroneous registration as costly as losing the chain secret, and it does not exist under BLS reveals, where there is nothing to misconfigure beyond the signing key itself. Validator clients should therefore verify a commitment by walking its full chain before registering, and treat the chain secret with the same care as the signing key.

### Quantum Adversaries

The scheme rests on two properties of BLAKE3. Grinding resistance rests on collision resistance: a proposer that could find a collision could give a chain link two preimages and choose between them at reveal time to bias its contribution.

Unpredictability rests on preimage resistance: a reveal cannot be derived from the public commitment before it is published. The post-quantum security of the scheme is therefore comparable to the collision resistance of BLAKE3, which is believed to remain adequate against a quantum adversary. The transitional BLS reveal path and the BLS signature on `RandaoCommitmentRegistration` remain quantum-vulnerable, as do block signatures generally; this EIP removes RANDAO's structural dependence on signature uniqueness so that the eventual post-quantum fork is a signature-scheme swap rather than a randomness redesign.

### Denial of Service via Registrations

Registration confers no randomness advantage, so spamming registrations is a pure-cost DoS vector. It is bounded on every surface:

- **Blocks.** The operations list is capped at `MAX_RANDAO_COMMITMENT_REGISTRATIONS` per block (at most 128 additional BLS verifications).
- **Gossip.** A validator registers at most once, so first-seen-per-validator deduplication (as for `bls_to_execution_change`) bounds gossip to one signature verification per validator, ever. Non-validators cannot participate: their messages fail signature validation and incur gossipsub peer penalties. Messages for an already-registered validator fail the head-state REJECT check, also penalized.
- **State.** `queue_randao_commitment` enforces at most one pending registration per validator, and the queue is additionally bounded by inclusion rates: at most `MAX_RANDAO_COMMITMENT_REGISTRATIONS` entries per block, each resident for `COMMITMENT_REGISTRATION_DELAY` epochs, giving a worst-case steady state of ~12,300 entries.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
