EIP-8321 -- The Beacon Chain
Note: This document is a work-in-progress for researchers and implementers.
Introduction
This upgrade replaces the BLS-signature RANDAO reveal with a hash-chain
commit-reveal scheme, as part of the EIP-8321 upgrade.
RANDAO's resistance to grinding currently relies on BLS signatures being
unique, so that a proposer cannot bias its contribution. A hash chain relies
only on standard hash-function security instead: collision resistance replaces
BLS's uniqueness, and preimage resistance keeps each reveal unpredictable until
it is published. Both are believed to hold against a quantum adversary.
Each validator generates a hash chain off-chain and registers its tip as a
commitment. When proposing, the validator reveals the preimage of its currently
stored commitment; the state verifies the chain step, folds the raw preimage
into the RANDAO accumulator, and stores the preimage as the validator's new
commitment. The state therefore holds one 32-byte word per validator and walks
one link back per proposal.
A commitment is registered once, through a new per-block-capped beacon
operation. It cannot be updated in place, and a validator index keeps its
commitment for as long as the index exists, so the binding is one chain to one
public key to one validator index. A validator that ever needs a new chain
therefore obtains one the same way it would recover from a lost signing key: by
onboarding a new validator under a new public key. Validators that have not
registered a commitment continue to use the legacy BLS reveal; this is a
transitional path intended for removal in a later upgrade.
Note: The deposit contract's fields are fixed and carry no commitment, so a
new validator cannot set one at deposit time; it registers through this
upgrade's operation like everyone else. Setting the commitment at deposit time
is deferred to a fork that already needs to change the deposit flow, such as the
one that retires the transitional BLS-signature path for a post-quantum scheme.
Retiring that path will also need a way to push already-onboarded validators who
never registered to do so, for example by no longer accepting blocks from them;
this upgrade does not specify that mechanism.
Note: This specification is built upon Heze.
Types
New PendingRandaoCommitments
| class PendingRandaoCommitments(ProgressiveList[PendingRandaoCommitment]):
"""
The queue of hash-chain RANDAO commitments awaiting activation.
"""
|
New RandaoCommitmentRegistrations
| class RandaoCommitmentRegistrations(ProgressiveList[SignedRandaoCommitmentRegistration]):
"""
The signed hash-chain RANDAO commitment registrations included in a beacon
block.
"""
|
New RandaoCommitments
| class RandaoCommitments(ProgressiveList[Bytes32]):
"""
The current hash-chain RANDAO commitment of every validator. A zero entry
means that the validator has not registered a commitment and reveals with
the legacy BLS signature instead.
"""
|
Constants
Domains
| Name |
Value |
DOMAIN_RANDAO_COMMITMENT_REGISTRATION |
DomainType('0x11000000') |
Hash chain
| Name |
Value |
HASH_CHAIN_RANDAO_DST |
b'HASH_CHAIN_RANDAO' |
UNSET_RANDAO_COMMITMENT |
Bytes32() |
Presets
Hash-chain RANDAO
| Name |
Value |
COMMITMENT_REGISTRATION_DELAY |
Epoch(3) (= 3) |
MAX_RANDAO_COMMITMENT_REGISTRATIONS |
Uint64(2**7) (= 128) |
Note: COMMITMENT_REGISTRATION_DELAY must be at least
MIN_SEED_LOOKAHEAD + 2 so that a registrant cannot know whether it proposes in
the activation epoch at the time its registration is included.
Containers
New containers
PendingRandaoCommitment
| class PendingRandaoCommitment(Container):
validator_index: ValidatorIndex
commitment: Bytes32
activation_epoch: Epoch
|
RandaoCommitmentRegistration
| class RandaoCommitmentRegistration(Container):
validator_index: ValidatorIndex
commitment: Bytes32
|
SignedRandaoCommitmentRegistration
| class SignedRandaoCommitmentRegistration(Container):
message: RandaoCommitmentRegistration
signature: BLSSignature
|
Modified containers
BeaconBlockBody
Note: randao_reveal is retained for validators that have not registered a
commitment. Exactly one of randao_reveal and hash_chain_reveal is populated;
the other is empty.
| class BeaconBlockBody(ProgressiveContainer(active_fields=[1] * 15)):
randao_reveal: BLSSignature
eth1_data: Eth1Data
graffiti: Bytes32
proposer_slashings: ProposerSlashings
attester_slashings: AttesterSlashings
attestations: Attestations
deposits: Deposits
voluntary_exits: VoluntaryExits
sync_aggregate: SyncAggregate
bls_to_execution_changes: BLSToExecutionChanges
signed_execution_payload_bid: SignedExecutionPayloadBid
payload_attestations: PayloadAttestations
parent_execution_requests: ExecutionRequests
# [New in EIP8321]
hash_chain_reveal: Bytes32
# [New in EIP8321]
randao_commitment_registrations: RandaoCommitmentRegistrations
|
BeaconState
| class BeaconState(ProgressiveContainer(active_fields=[1] * 48)):
genesis_time: Uint64
genesis_validators_root: Root
slot: Slot
fork: Fork
latest_block_header: BeaconBlockHeader
block_roots: BlockRoots
state_roots: StateRoots
historical_roots: HistoricalRoots
eth1_data: Eth1Data
eth1_data_votes: Eth1DataVotes
eth1_deposit_index: Uint64
validators: Validators
balances: Balances
randao_mixes: RandaoMixes
slashings: Slashings
previous_epoch_participation: EpochParticipation
current_epoch_participation: EpochParticipation
justification_bits: JustificationBits
previous_justified_checkpoint: Checkpoint
current_justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
inactivity_scores: InactivityScores
current_sync_committee: SyncCommittee
next_sync_committee: SyncCommittee
latest_block_hash: Hash32
next_withdrawal_index: WithdrawalIndex
next_withdrawal_validator_index: ValidatorIndex
historical_summaries: HistoricalSummaries
deposit_requests_start_index: Uint64
deposit_balance_to_consume: Gwei
exit_balance_to_consume: Gwei
earliest_exit_epoch: Epoch
consolidation_balance_to_consume: Gwei
earliest_consolidation_epoch: Epoch
pending_deposits: PendingDeposits
pending_partial_withdrawals: PendingPartialWithdrawals
pending_consolidations: PendingConsolidations
proposer_lookahead: ProposerLookahead
builders: Builders
next_withdrawal_builder_index: BuilderIndex
execution_payload_availability: ExecutionPayloadAvailability
builder_pending_payments: BuilderPendingPayments
builder_pending_withdrawals: BuilderPendingWithdrawals
latest_execution_payload_bid: ExecutionPayloadBid
payload_expected_withdrawals: Withdrawals
ptc_window: PTCWindow
# [New in EIP8321]
randao_commitments: RandaoCommitments
# [New in EIP8321]
pending_randao_commitments: PendingRandaoCommitments
|
Helpers
Crypto
New blake3
def blake3(data: bytes) -> Bytes32 is the BLAKE3 hash function in its default
unkeyed hash mode, with no derive-key context, restricted to its default 32-byte
output.
All hashing introduced by this upgrade uses blake3; the hash helper
continues to serve the legacy reveal path.
Validator registry
Modified add_validator_to_registry
Note: The function add_validator_to_registry is modified to initialize the
item in the randao_commitments list, preserving the invariant that it has one
entry per validator.
| 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))
# [New in EIP8321]
set_or_append_list(state.randao_commitments, index, UNSET_RANDAO_COMMITMENT)
|
RANDAO verifications
New verify_hash_chain_reveal
| def verify_hash_chain_reveal(
state: BeaconState, body: BeaconBlockBody, proposer_index: ValidatorIndex
) -> None:
"""
Verify that ``body`` reveals the preimage of the proposer's stored commitment.
"""
assert body.hash_chain_reveal != Bytes32()
assert body.randao_reveal == G2_POINT_AT_INFINITY
commitment = state.randao_commitments[proposer_index]
assert blake3(HASH_CHAIN_RANDAO_DST + body.hash_chain_reveal) == commitment
|
New verify_bls_randao_reveal
| def verify_bls_randao_reveal(
state: BeaconState, body: BeaconBlockBody, proposer_index: ValidatorIndex
) -> None:
"""
Verify that ``body`` reveals the proposer's signature over the current epoch.
"""
assert body.hash_chain_reveal == Bytes32()
epoch = get_current_epoch(state)
signing_root = compute_signing_root(epoch, get_domain(state, DOMAIN_RANDAO))
pubkey = state.validators[proposer_index].pubkey
assert bls.Verify(pubkey, signing_root, body.randao_reveal)
|
Beacon chain state transition function
Epoch processing
Modified process_epoch
Note: The function process_epoch is modified to call the new helper
process_pending_randao_commitments.
| def process_epoch(state: BeaconState) -> None:
process_justification_and_finalization(state)
process_inactivity_updates(state)
process_rewards_and_penalties(state)
process_registry_updates(state)
# [New in EIP8321]
process_pending_randao_commitments(state)
process_slashings(state)
process_eth1_data_reset(state)
process_pending_deposits(state)
process_pending_consolidations(state)
process_builder_pending_payments(state)
process_effective_balance_updates(state)
process_slashings_reset(state)
process_randao_mixes_reset(state)
process_historical_summaries_update(state)
process_participation_flag_updates(state)
process_sync_committee_updates(state)
process_proposer_lookahead(state)
process_ptc_window(state)
|
New process_pending_randao_commitments
Note: Commitments are queued with an activation epoch derived from the epoch
of inclusion, so the queue is ordered by activation epoch and can be drained
from the front.
| def process_pending_randao_commitments(state: BeaconState) -> None:
next_epoch = Epoch(get_current_epoch(state) + 1)
next_pending_commitment = 0
for pending_commitment in state.pending_randao_commitments:
if pending_commitment.activation_epoch > next_epoch:
break
index = pending_commitment.validator_index
state.randao_commitments[index] = pending_commitment.commitment
next_pending_commitment += 1
state.pending_randao_commitments = state.pending_randao_commitments[next_pending_commitment:]
|
Block processing
Modified process_randao
Note: A zero entry in randao_commitments means that the proposer has not
registered a hash chain, and the legacy BLS reveal applies. Rejecting a zero
reveal keeps that sentinel meaningful: without it, a validator revealing the
zero word would silently store the sentinel and fall back onto the legacy
branch.
Note: The hash-chain path folds in the raw reveal with a hash accumulator
rather than an xor. Commitments carry no identity and are copyable, so a
validator may register another's commitment. Doing so is self-defeating: the
copier does not hold the preimage, so it cannot propose at all until its victim
reveals, forfeiting every slot it is assigned in the meantime. Even once the
victim reveals, the accumulator has no efficiently computable inverse, so
re-injecting the copied reveal produces an unrelated mix rather than cancelling
the victim's contribution, which an xor accumulator would have allowed.
| def process_randao(state: BeaconState, body: BeaconBlockBody) -> None:
epoch = get_current_epoch(state)
proposer_index = get_beacon_proposer_index(state)
# [New in EIP8321]
if state.randao_commitments[proposer_index] != UNSET_RANDAO_COMMITMENT:
verify_hash_chain_reveal(state, body, proposer_index)
mix = blake3(get_randao_mix(state, epoch) + body.hash_chain_reveal)
state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mix
state.randao_commitments[proposer_index] = body.hash_chain_reveal
else:
verify_bls_randao_reveal(state, body, proposer_index)
mix = xor(get_randao_mix(state, epoch), hash(body.randao_reveal))
state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mix
|
Operations
Modified process_operations
Note: The function process_operations is modified to process RANDAO
commitment registrations.
| def process_operations(
state: BeaconState,
body: BeaconBlockBody,
parent_slot: Slot,
) -> None:
assert len(body.deposits) == 0
def for_ops(operations: Sequence[Any], fn: Callable[..., None], *args: Any) -> None:
for operation in operations:
fn(state, operation, *args)
assert len(body.proposer_slashings) <= MAX_PROPOSER_SLASHINGS
assert len(body.attester_slashings) <= MAX_ATTESTER_SLASHINGS_ELECTRA
assert len(body.attestations) <= MAX_ATTESTATIONS_ELECTRA
assert len(body.voluntary_exits) <= MAX_VOLUNTARY_EXITS
assert len(body.bls_to_execution_changes) <= MAX_BLS_TO_EXECUTION_CHANGES
assert len(body.payload_attestations) <= MAX_PAYLOAD_ATTESTATIONS
# [New in EIP8321]
assert len(body.randao_commitment_registrations) <= MAX_RANDAO_COMMITMENT_REGISTRATIONS
for_ops(body.proposer_slashings, process_proposer_slashing)
for_ops(body.attester_slashings, process_attester_slashing)
for_ops(body.attestations, process_attestation, parent_slot)
for_ops(body.voluntary_exits, process_voluntary_exit)
for_ops(body.bls_to_execution_changes, process_bls_to_execution_change)
for_ops(body.payload_attestations, process_payload_attestation)
# [New in EIP8321]
for_ops(body.randao_commitment_registrations, process_randao_commitment_registration)
|
RANDAO commitment registrations
New process_randao_commitment_registration
This is the one-time path that moves a validator from the legacy BLS reveal onto
its hash chain. It is valid only while the validator is unregistered, and only
while it has no other registration in flight. Nothing ever clears an entry of
randao_commitments, so a registered validator index keeps its chain for as
long as the index exists; replacing a lost chain means onboarding a new
validator under a new public key.
Note: Both checks are needed to make registration single-use. Once a
registration has activated, the stored commitment is non-zero and any replay
fails the first check. While it is still pending, the stored commitment is zero,
and the second check rejects the duplicate. The second check also rejects a
duplicate within a single block, since the first operation of the block appends
its entry to the queue.
Note: The activation delay is derived from the epoch of inclusion, the only
event the state observes. Signing a registration earlier only means committing
with less information.
| def process_randao_commitment_registration(
state: BeaconState, signed_registration: SignedRandaoCommitmentRegistration
) -> None:
registration = signed_registration.message
index = registration.validator_index
assert index < len(state.validators)
assert registration.commitment != UNSET_RANDAO_COMMITMENT
assert state.randao_commitments[index] == UNSET_RANDAO_COMMITMENT
assert all(pending.validator_index != index for pending in state.pending_randao_commitments)
# Fork-agnostic domain since registrations are valid across forks
domain = compute_domain(
DOMAIN_RANDAO_COMMITMENT_REGISTRATION,
genesis_validators_root=state.genesis_validators_root,
)
signing_root = compute_signing_root(registration, domain)
validator = state.validators[index]
assert bls.Verify(validator.pubkey, signing_root, signed_registration.signature)
state.pending_randao_commitments.append(
PendingRandaoCommitment(
validator_index=index,
commitment=registration.commitment,
activation_epoch=Epoch(get_current_epoch(state) + COMMITMENT_REGISTRATION_DELAY),
)
)
|