Ethereum Validator Lifecycle:
A Deep Dive

Author(s): Sergey Boogerwooger, Dmitry Zakharov
Security researcher(s) at MixBytes
Intro
Ethereum today implements the most decentralized proof-of-stake consensus. The current number of active validator keys exceeds 10^6, and each block is attested by hundreds of validators. The Eth 2.0 validating infrastructure is complex, split into beacon and execution layers, and includes many interesting mechanics. This intricate system is continuously upgraded "on-the-fly," which adds significant complexity. With that in mind, we decided to fill the gap between the higher-level documentation about Ethereum's consensus, economic security, and validation, and the actual code running in real production. The goal of this article is to give readers a comprehensive understanding of how an Ethereum validator works and provide links to the real code for those who want to dive deeper.

This article is not intended as a guide to estimate validator rewards. As you will see, there are too many factors in Ethereum's consensus to provide a straightforward "algorithmic" answer to this question. Factors such as the number of validators, the behaviour of effective balances, deposits, exits, withdrawals, slashings, bugs, and many other factors influence validators' rewards. These answers are better uncovered by analyzing real validator behavior and statistics. If you need a financial analysis of validator upkeep, it's more practical to explore resources that aggregate information about validator activity, such as beaconcha.in.

So, we have a validator and want to participate in the most decentralized consensus in the world. Let's begin!
Higher Level Review
Before diving deeper into the details of a validator's lifecycle, let's first describe it at a higher level. The Beacon validator cycle begins when a validator is promoted to active status. The cycle includes joining the queue, being accepted (or rejected), starting operations, becoming one of the epoch validators, validating and attesting incoming blocks during the epoch, building and publishing new blocks, receiving rewards for the previously published blocks and attestations, losing balance in the case of slashings, and finally exiting.

There are various implementations of Beacon chain validators across multiple programming languages. You can find their list and links in the official Ethereum documentation here. In this article, we will focus on the Lighthouse implementation and its documentation.

It is important to note that Ethereum 2.0 consensus is constantly evolving. Many parameters, limits, and logic components change with each hardfork. Several Beacon chain hardforks have already occurred, which we will reference in this article. At the time of writing, there have been four main hardforks, with two more planned for implementation. The specifications for these can be found here. These hardforks modify the validation logic, including updates to constants, time windows, penalties, limits, and the Beacon chain state structure. Some hardforks even enable or disable entire processes (e.g., deposits or withdrawals). When possible, we will highlight these changes as they relate to validator states. For now, let's briefly describe the main changes introduced in these hardforks and their timeline, as we will need to reference these names in later sections of the article.
Phase0 – The preparation phase for future upgrades, introducing the main Beacon chain state and Proof of Stake (PoS) design. Most of the foundational specifications were established in this hardfork.
Altair – Continued preparations for the migration from Proof of Work (PoW) to PoS. This hardfork introduced the Light Client Protocol and the "sync committee," a special subset of 512 validators chosen pseudorandomly during each "sync committee period" (~1 day). These validators continually sign new Beacon block headers, enabling light clients "outside" the Beacon chain to verify the consensus state with significantly reduced complexity.
Bellatrix – This hardfork marked the official transition to PoS, using the specifications introduced in earlier hardforks. It acted as the "turn-on" switch for "The Merge."
Capella – This hardfork enabled validator withdrawals, which had been disabled in previous versions. It finalized the full economic model of Ethereum PoS by allowing validators to withdraw their rewards.
Deneb – The latest hardfork at the time of writing this article (Jan 2025). It added the Beacon block root to the Ethereum Virtual Machine (EVM), an important change for cross-chain proofs of Ethereum consensus and changed some values, related to attestations and activation/exits limits.
Electra – A future hardfork will increase the maximum effective balance of validators from 32 ETH to 2048 ETH, allowing the consolidation of many redundant validators. Additionally, it introduces significant changes to deposits and withdrawals, making them faster and more flexible.
Fulu – Currently under construction.
Do not confuse these hardforks with those of the execution layer, such as Paris, Berlin, or Shanghai. In Ethereum 2.0, the Beacon chain interacts with the execution layer by outsourcing block processing to the execution layer, as well as "extracting" and "injecting" deposits and withdrawals. It is designed to prepare for the future capability of Ethereum's Beacon Chain to interact with multiple execution layers (sharding). Therefore, the logic of these layers can be updated independently. This results in pairs of complementary hardforks, such as "Shanghai/Capella = Shapella" or "Prague/Electra = Pectra." To help remember beacon chain hardforks sequence, note that the first letters of Beacon chain hardfork names follow alphabetical order: (A)ltair, (B)ellatrix, (C)apella, (D)eneb and so on.

The scope of these updates is extensive, and many sections of this article are tied to specific hardforks. We will strive to clarify these connections in the appropriate sections.
Beacon State
First, we need to review the key structure that holds information about the beacon chain state: the BeaconState. We will omit certain changes introduced by hardforks in this structure to better illustrate the data contained in BeaconState:
class BeaconState(Container):
# Versioning
genesis_time: uint64
genesis_validators_root: Root
slot: Slot
fork: Fork

# History
latest_block_header: BeaconBlockHeader
block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]

# Eth1
eth1_data: Eth1Data
eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
eth1_deposit_index: uint64

# Registry
validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]

# Randomness
randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]

# Slashings
slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR]

# Participation
previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]

# Finality
justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH]
previous_justified_checkpoint: Checkpoint
current_justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint

# Inactivity
inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT]  

# Sync committee [New in Altair]
current_sync_committee: SyncCommittee
next_sync_committee: SyncCommittee

# Execution [New in Bellatrix]
latest_execution_payload_header: ExecutionPayloadHeader  

# Withdrawals [New in Capella]
next_withdrawal_index: WithdrawalIndex
next_withdrawal_validator_index: ValidatorIndex
The sections of the Beacon State that are particularly important for us are:
# Eth1 , which contains the latest agreed-upon execution layer state.
# Registry , holding the list of validators and their staked balances.
# Slashings , which tracks total slashing amounts per epoch.
# Pariticipation , covering validators' participation in attestations.
# Finality , which provides data regarding finalization status.
# Withdrawals , holding details about the withrawals.
An excellent annotation of the Beacon State, along with its changes across hardforks, is available here. It is highly recommended to review this part of documentation before continuing.

The Ethereum blockchain progresses on a beacon node through a consistent update of the Beacon State. These updates modify state roots, validator lists, attestations, and, finally, the finality status based on new data received from the p2p network. The behavior of the validator state can be described as an interaction with specific parts of the Beacon State.

For a practical look at the Beacon State structure, you can explore its implementations in Lighthouse or Prysm.

[NOTE] In this article, we will frequently refer to the code of a beacon chain validator node. While we cannot fully describe all the underlying logic, we will highlight parts that are most relevant for demonstration purposes. In some cases, we will present code that illustrates various aspects of a validator’s lifecycle, even if it does not directly pertain to actual consensus checks. This is because these parts are often more informative. Sometimes, we will encounter different variants of the implementations of the same actions (“single pass”/“with cache” or "beacon layer"/"execution layer", etc…). For each case, we'll try to focus on the most demonstrative pieces of code.
Validator entity
First, let's discuss the validator entity. The primary identifier of a validator is its public BLS key (example of usage of the pubkey as an identifier is here). Beacon chain validators do not use Ethereum's standard secp256k1 ECDSA signatures (and addresses). Instead, they use BLS signatures. This choice was made because BLS signatures allow the aggregation of multiple signatures into a single signature. This makes it possible to verify hundreds or even thousands of pre-aggregated signatures using just one aggregated signature, significantly improving the efficiency of block "replay". Additionally, the deterministic property of BLS signatures enables their use for pseudorandom shuffling of validators, facilitating the creation of an unpredictable validator schedule (e.g., selecting groups of validators for each slot of the next epoch).

Compared to traditional secp256k1 signatures, BLS signatures are smaller in size and have a fast signing process, though their verification is slower. However, this slower verification is mitigated by the efficiency of signature aggregation. A detailed explanation is available in eth2book here or on the IETF site. There are also numerous articles about BLS signatures, which are widely used in threshold schemes and various consensus algorithms.

An example struct illustrating the validator lifecycle can be seen here:
pub struct Validator {
    pub pubkey: PublicKeyBytes,
    pub withdrawal_credentials: Hash256,
    pub effective_balance: u64,
    pub slashed: bool,
    pub activation_eligibility_epoch: Epoch,
    pub activation_epoch: Epoch,
    pub exit_epoch: Epoch,
    pub withdrawable_epoch: Epoch,
}
Withdrawing stake and rewards requires the withdrawal_credentials field, which can be in BLS or Eth1 format(added in Capella hardfork). This field represents a BLS or secp256k1 public key and is defined with the prefix 0x00 (BLS) or 0x01 (Eth1). This is further described here.

Another important field is effective_balance, which represents the validator's "active" balance and determines its influence in the protocol. Like in other staking protocols, this balance serves as the validator's "weight." In Ethereum 2.0, the effective balance of our validator is used to calculate:

  • The probability of being selected as a block proposer
  • The validator's weight in the LMD-GHOST fork choice rule
  • The validator's weight in justification and finalization calculations
  • The probability of inclusion in a sync committee
  • The slashing and inactivity penalty amounts

Although block, attestation, slashing, and sync committee inclusion rewards use the same base reward for all validators, greater participation probability due to a higher effective balance results in higher cumulative rewards. This probability is proportional to the effective balance of a validator and thus plays an important role in the consensus. Currently (between Deneb and Electra), the maximum effective balance is 32 ETH, but after the Electra hardfork, the effective balance limits will change from 32 to 2048 ETH.

The effective balance tracks a validator's actual balance (which increases through deposits and rewards and decreases due to penalties and slashing) but is updated less frequently—only once per epoch—unlike the actual balance, which changes with each block when attestation rewards, slashing, or inactivity penalties are applied.

In addition, the effective balance is limited to multiples of spec.effective_balance_increment (1 ETH in current mainnet spec). Effective balances are stored in Gwei's, allowing to use u64 type for them. In addition, the frequent changes in our validator's actual balance(per block) influence our effective balance much less frequently (once per epoch), when it is needed for generating the next epoch's validator schedule. The changes in effective balance are "stuck" to the nearest multiple of spec.effective_balance_increment (1 ETH). This "hysteresis" process is demonstrated in the picture below:

More information about the hysteresis for effective balances can be found here, the parameters, controlling the hysteresis in chain specs can be found here. In the current pre_electra hard forks, the effective balance of a validator is capped at 32 ETH. After the Electra hardfork, this cap will be removed (capped by 2048 ETH). Validators will then be able to operate with larger stakes, though the hysteresis rules will remain applicable.
Now, to make the next sections of this article easier to follow, we will first provide a high-level overview of "per-epoch" and "per-block" processing procedures. This will simplify understanding the next topics. Let's begin with "per-epoch."
Per-epoch processing
An epoch is a numbered time interval (measured in block numbers) consisting of 32 blocks (~6.5 minutes at present). Each epoch has a fixed pseudorandomly assigned set of validator committees for each slot, responsible for proposing and attesting blocks. Activities such as the activation and exit of validators, scheduling (assigning validators to committees), and other operations related to validator grouping are predominantly carried out on a "per-epoch" basis.

A demonstration of "per-epoch" processing can be found in the process_epoch_single_pass() function. This section provides a high-level overview of these operations. They include:
Loading and initialization of beacon state information. This involves previous, current, and next epochs, the last finalized checkpoint(epoch), the total active balance of validators, the inactivity_leak flag (indicative of issues in chain finalization), churn limits for entering and exiting validators, and other important values.
Loading slashing context information (containing records of previously slashed validators), pending deposits, information about the earliest exit epoch (validators cannot exit immediately), and the consumed balances of exiting validators.
Processing the activation queue for validators scheduled to become active (this process will change significantly following the Electra hardfork).
Loading info about validators, their balances, inactivity scores and participation in current and previous epochs.
Iterating through all validators of the previous and current epochs, performing the following steps:
Loading validator-specific data such as balance, inactivity scores, and information about participation in previous and current epochs
Determining whether a validator is eligible for activation and calculating its base_reward. If a validator participated in the previous epoch without issues the base_reward will be non-zero.
Processing and updating inactivity scores. Persistent inactivity leads to increasing penalties for all inactive validators when finality cannot progress, causing the beacon chain to enter "inactivity leak" mode.
Calculating rewards and penalties for each validator.
Updating the registry, including handling validator inclusions and exclusions, working with activation and exit queues, and adjusting balances.
Slashing validator if needed. This is a "per-epoch" part of slashing, there is also an "immediate" part of slashing within the "per-block" processing.
Processing pending deposits for this validator arriving from the execution layer.
Updating final balances for validators.
(end of loop through validators)
Processing the global earliest exit epoch and exit balances.
Finalizing the processing of pending deposits.
Consolidating and finalizing updates to effective balances for all validators.
As can be seen, this "per-epoch" processing encompasses tasks related to consolidated operations such as effective balance updates, validator participation updates, the activation queue, and other operations that do not require an immediate reaction in each block. This stage forms a complex and resource-intensive part of the consensus process. Now, let us move on to analyze the "per-block" processing part.
Per-block processing
This stage of validation occurs much more frequently than "per-epoch" processing and involves the following operations:

  • Direct processing of beacon blocks
  • Management of validator operations that require immediate action, such as slashings, inactivity scores update or rewards
  • Handling of operations that transfer information between the Eth1 execution layer (ETH 1.0 blocks) and the beacon chain, and vice versa

The demonstrative pseudocode structure used for "per-block" operations is represented by the BeaconBlockBody:
class BeaconBlockBody(Container):
    
    # pseudorandom seed, used for validators shuffling
    randao_reveal: BLSSignature

    # data from Eth1 execution layer, related to ETH deposits
    eth1_data: Eth1Data

    # arbitrary string, provided by block proposer, the "graffity"
    graffiti: Bytes32

    # Operations
    
    # messages, proving slashings of validators-proposers
    proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
    
    # messages, proving slashings of validators-attesters
    attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS]
    
    # signatures of validators for chain checkpoints
    attestations: List[Attestation, MAX_ATTESTATIONS]
    
    # messages, proving deposits of validators
    deposits: List[Deposit, MAX_DEPOSITS]
    
    # messages, proving requests to exit
    voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]
    
    # aggregated data about chain head state from sync committee members
    sync_aggregate: SyncAggregate

    # Execution
    
    # main execution layer(Eth1) block data
    execution_payload: ExecutionPayload
    
    # messages proving change of the withdrawal credentials of a validator
    bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES]
Operations, interesting for us are presented in the # Operations section, and described here. Next part of demonstrative pseudocode for "per-block" processing:
def process_block(state: BeaconState, block: BeaconBlock) -> None:
    process_block_header(state, block)

    if is_execution_enabled(state, block.body):
        process_withdrawals(state, block.body.execution_payload) # [New in Capella]
        process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) # [Modified in Capella]

    process_randao(state, block.body)
    process_eth1_data(state, block.body)
    process_operations(state, block.body) # [Modified in Capella]
    process_sync_aggregate(state, block.body.sync_aggregate)
Let’s analyze the code for block processing, beginning with the function per_block_processing(), which includes the processing of:
Block header: Verification of block header and all signatures
Withdrawals: Process withdrawals, decreasing validators' active balances
Execution payload: This part handles the processing of the actual Eth1 state. Most of the work is delegated to the execution client, which executes transactions, updates states, logs, the Eth1 block hash, and then sends the resulting Eth1 header info back to the beacon chain. However, key checks for the previous hash, timestamp, and randao parameters are performed here.
Randao: This provides the randomness seed for subsequent blocks. It is derived from the aggregated BLS signature, providing a reliable source of randomness for shufffling validators. Previous randao mixes are stored to allow reconstruction of validator index lists for earlier blocks.
Eth1 data: Currently, this is mostly used to handle deposits from validators, though this may change significantly in future hard forks.
Operations: These include proposer and attester slashing, attestations, deposits, and voluntary exits. These operations update validator balances and statuses.
Sync committee: A small subset of validators tasked with providing lightweight information about the chain head to light clients.
We will explore all these operations in detail in the following sections, starting with the initial step in the validator lifecycle — activation. Let’s dive in.
Validator Activating
The first step to becoming a validator is, of course, staking. This is accomplished through the special deposit contract, whose code can be found here. The contract is deployed at this address, where we can see deposits of 32 ETH each (Note that these amounts will change after the upcoming Electra hardfork, allowing effective balances of up to 2048 ETH).

The important step in the process is passing the validator's public key. There are no checks for the signature provided to the contract; all data is simply added to the Merkle tree of deposits. These checks are deferred to the Beacon layer. Additionally, the deposit contract does not send ETH back because validator deposits are later processed at the Beacon layer, not the execution (Eth1) layer.

The most critical function of this contract (until EIP-6110) is emitting the Deposit (pubkey, withdrawal_credentials, amount, ...) event. The logs of Eth1 blocks are continuously parsed in the Beacon layer and then processed, including a BLS signature validation.

Deposits from potential validators are included in an incremental Merkle tree, which can be reconstructed by parsing events from the deposit contract. Proofs of inclusion are required at the consensus (Beacon) layer to verify the validity of deposits. These mechanics are detailed here. The first deposit from any validator requires a signature to prove ownership of the corresponding private key. Interestingly, subsequent deposits for the same validator do not require this signature, allowing anyone to top up the validator's balance.

Our target is to reach the add_validator_to_registry() function, which adds the validator to the self.validators list. This action is performed in the apply_deposit() function. Here, the signature is verified, and the deposit is added to a "pending deposits" list (this branch). This function also verifies the Merkle proof of deposit inclusion, as mentioned earlier.

It is also worth noting that the current interaction between the execution and consensus layers via log parsing is not very efficient. There is an ongoing proposal (EIP-6110) to handle all deposit logic directly in the Eth1 blocks, skipping interactions with deposit contract. This change could significantly reduce the current delays in deposit inclusion. Furthermore, currently, any deposit with an invalid signature is ignored and cannot be refunded, as the consensus layer does not directly interact with the execution layer.

Submitting a deposit and its inclusion in the validators list does not activate a validator immediately (recall that the deposit is placed in the "pending" deposits queue). Validators must go through the activation queue. While deposits are processed at every execution block (to ensure no events are missed), validator status updates occur "per-epoch" (as shown in the processing). At this stage, the activation_churn_limit parameter determines how many validators can be taken from the queue per epoch. This limit is capped by the 1 / 65536 (spec.churn_limit_quotient (65536)) fraction of the total validator count. The minimal value is spec.min_per_epoch_churn_limit (4), which is also used for validator exits, regulating the whole "churn limit" for both validator activations and exits. If the activation queue is full, a validator will wait for the next epochs to become eligible for activation.

We previously encountered registry_updates here. Now, let's explore this procedure in greater depth. We see two branches, pre_electra and post_electra. Both branches first verify the validator's eligibility by checking its effective balance through these "pre_electra" and "post_electra" checks. For "pre_electra," the effective balance must be exactly 32 ETH, while for "post_electra," it must be greater or equal to 32 ETH (defined by spec.min_activation_balance(32 ETH)).

Both functions trigger the automatic exit of validators whose effective balance falls below spec.ejection_balance(16 ETH).

Next comes the main "activation" process, which assigns the validator.activation_epoch. In "pre_electra," this is done through the activation queue (here), while in "post_electra," it is processed without the queue by verifying that eligibility was established in finalized blocks (here).

Finally, our validator becomes active at the validator.activation_epoch. This epoch is determined by incrementing the current epoch and adding the spec.max_seed_lookahead (4 epochs, ~25.6 minutes).
Validator Exiting
Now, let's review how an active validator exits, as it is closely related to activation. There are two types of exits: forced and voluntary. Let's start with forced exits:

In the registry_updates ("per-epoch" processing), we observe that if validator.effective_balance < spec.ejection_balance (check here) for each active validator (already mentioned above), an exit is initiated. This same check also exists in the "per-validator" registry update here.

Another unconditional exit occurs if a validator is slashed.

Voluntary exits require more complex actions. They can be initiated through the node's CLI (see here). A BLS signature from the validator is required, which creates a simple signed "exit" message: VoluntaryExit.

The verification and processing of voluntary exits occur during "per-block" operations within the process_exits() function. This involves calling verify_exit(), which is of primary interest to us. It begins with the "is active" check (activation_epoch <= current_epoch <= exit_epoch) and verifies that the validator is "not currently exiting." This is done via the exit_epoch, which, for active validators, equals spec.far_future_epoch (u64::MAX).

Next, the exit epoch checks are performed, including the "too young to exit" check. This denies exit for a validator who started at the activation_epoch but has not worked for at least spec.shard_committee_period (256 epochs). Following this is the signature check, where the voluntary exit must be BLS-signed by the validator.

An additional check introduced in "post_electra" ensures the absence of pending withdrawals.

Both voluntary and forced exits ultimately lead to initiate_validator_exit(), where the exit (queue) epoch is calculated. Here we also encounter two branches: "pre_electra" and "post_electra." Based on the prior activation logic, it is clear that the Electra hardfork will simplify the activation and exit procedures by removing activation and exit queues.

In the current "pre_electra" branch, the earliest exit epoch is determined, starting at ++current_epoch + spec.max_seed_lookahead (+4 epochs). This uses the exit queue and the same "validator churn limit", restricting the abrupt exits or activations of large numbers of validators.

In the "post_electra" branch, the exit epoch is calculated, also starting at ++current_epoch + spec.max_seed_lookahead. However, it searches for the first "free to exit" epoch using "balance churn limits"(recall that Electra introduces arbitrary effective balances for validators). This restricts the amount of stake being removed instead of the number of validators, maintaining the economic security of the protocol. This logic resides in the function compute_exit_epoch_and_update_churn().

Both churn limits are dynamic and are calculated based on the total number of validators and their total effective balance. They use the same spec.churn_limit_quotient (65536) quotient, allowing the exit of no more than 1/65536 of the total effective balance of validators in a single epoch. Thus, the earliest exit epoch for a validator is ++spec.max_seed_lookahead (5 epochs, ~32 minutes), although this value may be larger if the exit queue is full or the amount of exiting stake is substantial.

Finally, validator.exit_epoch and validator.withdrawable_epoch are saved, and the "per-epoch" counters of exiting validators are updated in the beacon state.

With the validator exited, the beacon state is updated with the earliest exit epoch (based on churn limits) and the total exit balances to include exiting validators.

We can now proceed to the operations of active validators.
Active validator
Validator selection for epoch
We now have a validator that satisfies the is_active_at(current_epoch) check. The list of active validators for the current epoch is generated in get_active_validator_indices(). Next, our validator must be assigned to one of the committees (each validator belongs to only one committee). Each slot(block) and epoch(identified by special "checkpoint" blocks) is attested by many committees of validators.

The amount of "per-slot" committees is defined in the get_committee_count_per_slot_with() function, which calculates the required number of committees using the total number of validators and spec.target_committee_size (128), while ensuring there are no more than
spec.max_committees_per_slot (64) committees per slot, with 128 validators in each committee.

Now we are ready to form committees for the current epoch. Let's take a look at the initialization of the committee cache here. Here, we see the initialization of RANDAO parameters, which serve as the seed for shuffling validators to deterministically assign them to different committees pseudorandomly. These RANDAO seeds enable the deterministic reconstruction of validators' committees for previous blocks without requiring access to the contents of those blocks. This is why the validator retains past RANDAO seeds in a fixed-size circular list for EpochsPerHistoricalVector (65536 epochs, ~290 days), which allows, for example, recalculating past committees for slashing purposes.

Next in the process is the shuffling of the validator list. Finally, we obtain the indices of the validators for the current epoch and their per-slot distribution. The shuffling algorithm, based on the "swap-or-not" shuffle, makes it possible to efficiently generate disjoint shuffled lists for subsets of validators without needing to shuffle the entire validator list and extract the subsections. You can read more about the algorithm here. Thus, for each epoch, there are slots, unique validator committees for each slot, and opportunities for the validator to join an attesting committee or act as a block proposer.

Shuffling is also used for the selection of block proposers and sync committee participation, with the probability of these events depending on the validator's effective balance. We will revisit this fact later.
Validator Proposing
Proposing a block is one of the most complex tasks for a validator. This is not only because it requires processing and validating the entire Eth1 state, but also because Ethereum is currently in a transitional phase. In this phase, "old" Eth1 blocks still exist largely in their original form, even as their design becomes increasingly outdated with every new hardfork. This legacy logic includes many branches that are planned for removal or revision. While we won’t delve deeply into block processing, as this is expected to change significantly in upcoming hardforks, our focus remains on reviewing the validator states and lifecycle.

Block proposals occur very infrequently. You can check block proposals for validators such as 1 (grandpa) or 781242 (top validator today). Currently, the interval for block proposals by a single validator spans hundreds of days. However, it’s essential to understand that as of early 2025, there are over 10^6 validator keys. These are not all tied to individual servers; one validator node can operate with thousands of keys, dynamically switching signing credentials. As a result, there are only thousands of "physical" validators managing many validator keys. After the Electra hardfork, which will remove the fixed effective balance requirement, many of these validators are expected to consolidate.

The block proposer is selected using this function. It picks a validator from a shuffled list and incorporates balance-depending randomness, increasing the probability of selection for validators with a greater effective balance.

Our goal is not to examine the details of block building, as it is a complex process comprising several asynchronous and blocking parts. However, you can explore this process yourself. The core function for building the next beacon block is available here and consists of two main components: produce_partial_beacon_block() and complete_partial_beacon_block(). Significant parts of these functions include:
produce_partial_beacon_block():
Building the committees cache for the current slot.
Loading the execution payload from the execution layer client.
Receiving validator slashing and exit messages from the pool.
Getting deposits from the execution layer.
Gathering and filtering attestations for previous blocks.
Re-checking the signatures of attestations, slashings, and exit messages (if "paranoid" mode is enabled).
complete_partial_beacon_block():
Producing the block object required for the specified hardfork.
Computing the block reward (we will discuss this later).
Per-block processing, which includes signature checks for attestations, slashings, and randao, a block signature check, and other critical verifications essential for consensus security.
Validation of KZG blob commitments introduced in the Deneb-Cancun hardfork.
Finally, the constructed but unsigned block is signed and published.

The computation of block rewards in Eth 2.0 is multifaceted. For a deeper understanding, see this function, which outlines rewards derived from:
Participation in the sync committee. The logic for this reward introduced in the Altair hardfork can be found here. The proposer reward logic applies if the proposer participates in the sync committee.
Proposer slashing inclusion, where a portion of the slashed validator's effective balance is seized.
Attester slashing inclusion, with logic similar to proposer slashings
Attestations inclusion. The latest Altair/Deneb version of this reward calculation is available here.
The rewards described above pertain to the consensus layer. Each execution layer block also includes transaction fees and MEV rewards, which are not the focus of our article.

A complex rewards estimation requires analyzing historical data from many validators, which is beyond the scope of this discussion. Instead, we leave such analyses to specialized teams. However, to make the provided code references more enlightening, here is an example of aggregated rewards data collected from ~600k validators. For the source, refer to this link:

Now, let's proceed to the attestations, which are the primary task of our validator.
Validator attesting
This activity forms the core function of a validator, where it attests to blocks proposed by other proposers and earns rewards for each successful attestation. In every slot, a proposer proposes a block. All committee members then attest to what they identify as the current head of the chain. Ideally, this is the block just proposed using the fork-choice rule.

Not every block is used for voting, as attestations are based on "checkpoints", specifically selected blocks, typically one per epoch. In simple terms, a checkpoint is defined as "a block in a fixed slot of the current epoch or the preceding nearest block, which may belong to a prior epoch". Checkpoints are essential because consensus mechanisms justify epochs rather than individual blocks. Certain blocks, serving as "epoch anchors," play this role, though they may sometimes be skipped. In such cases, one of preceding blocks is selected. So, there can be situations when a block B from epoch j-1 is used as a checkpoint for epoch j, because the newer epoch j doesn't have its own blocks that can serve as an "anchor".

An epoch becomes justified if its checkpoint attains votes representing more than two-thirds of the staked balance. You can delve deeper into the consensus algorithm and "(block, epoch) pairs" in the Gasper whitepaper.

Gasper consensus consists of two main parts:

  • Casper FFG (Casper the Friendly Finality Gadget), responsible for the justification and finalization of previously produced blocks and epochs.
  • LMD GHOST Fork-Choice rule (Latest Message Driven Greediest Heaviest Observed SubTree), responsible for the selection of the chain head (the "best fork") to build the next block upon.

Both parts require validators to vote for checkpoints in previous epochs and current blocks. In the current Beacon Chain, they do this simultaneously, voting for the head block and the last justified epoch in the same message. That's why the struct AttestationData includes beacon_block_root (the beacon block hash used by the LMD GHOST fork-choice rule) as well as source and target checkpoints (used in the FFG finality gadget). This structure is explained in detail here.

Multiple attestations, signed with BLS signatures, can be aggregated with other signatures, condensing numerous individual signatures into a single signature. This is where BLS truly demonstrates its utility; it would be infeasible to process the chain's state without aggregation given the large number of validators and signatures that must be verified.

Validator attestations must comply with various aging rules, which depend on the age of the attestation (e.g., it must not be newer than spec.min_attestation_inclusion_delay (1) slots or older than the previous epoch (changed in Dencun hardfork in EIP-7045)). They must also include a valid signature (or an aggregated signature, as new attestations can aggregate earlier ones). The process for creating unaggregated "initial" attestations can be reviewed in produce_unaggregated_attestation(), where the target checkpoint is chosen based on the known chain's "head." Additional validity checks are performed here, followed by retrieval of the last justified checkpoint here to serve as the source checkpoint.

At this stage, the validator has determined the source checkpoint it recognizes as justified, identified the "new" checkpoint block in the current epoch as the "target" checkpoint, and cast its vote.

Attestation rewards are structured, with varying weights assigned to attestations depending on whether there is a supermajority for the source checkpoint, target checkpoint, and the head block. Detailed descriptions can be found here and in this comprehensive study. For those interested in the code, the function compute_attestation_rewards_altair() provides a good example. Within this function, you will find a loop iterating over all validators, where rewards for "head," "source," and "target" votes are calculated separately. Consequently, reliable network connectivity and sufficient computational resources are vital for receiving and processing accurate blockchain head and attestation data. Without these, a validator risks forfeiting rewards.
Validator doing nothing
This state occurs when an active validator is offline, failing to fulfill its attestation and block proposal duties. In this scenario, the validator forfeits rewards and faces penalties.

On a "per-epoch" basis, each validator's inactivity score is updated. During the "per-validator" loop, if the validator is found to have been inactive in the current and previous epochs, its inactivity score is updated here. Specifically, the score is decreased by "1" or increased by spec.inactivity_score_bias (4) if the validator did not participate or was slashed. From the inactivity point of view, a slashed validator is treated similarly to a regular offline validator, with the same inactivity score penalties applied.

Additionally, all validators receive a constant reduction to their inactivity score by spec.inactivity_score_recovery_rate(16) when the chain is not in an "inactivity leak" state. This allows for the rapid recovery of inactivity scores once the chain resumes successful finalization.

The accumulated inactivity score results in penalties here. The exact penalty amount is determined here. While penalties are skipped for validators that participated, they are applied to inactive or slashed validators. The penalty is calculated as a fraction of the validator's balance: inactivity_score / (inactivity_penalty_quotient_for_fork (2^24) * inactivity_score_bias (4)).

More details on inactivity penalties can be found here.
Validator slashing
Slashing is a crucial component of any proof-of-stake consensus mechanism. There are two main types of slashing: proposer slashings and attester slashings. The most critical behavior of an attacker from a consensus perspective is building two conflicting chains, which can result in a "double spend" attack. Another type of attack on network liveness that slashings aim to mitigate is the balancing attack.

Our validator performs slashing using a specialized slasher service that monitors beacon chain blocks and attestations. The slasher service processes new blocks and checks them by searching for blocks with the same slot but different headers. In such cases, it identifies proposer slashings and publishes them. These slashings are then processed and included in subsequent blocks by proposers (this process was previously mentioned in the "proposing" section here).

Similarly, attestations are processed in the slasher through the process_attestations() function. Since this involves handling a large number of entities—some of which may be out-of-date—the first step is to filter out obsolete attestations. Next, the attestation batches are grouped by validator, and each attestation in a batch is checked for:

  • Double voting, where the same validator signs two conflicting blocks (similar to proposer slashing).
  • Surrounding voting, where the source and target votes in one attestation "surround" those in another attestation from the same validator.

These slashing conditions are further described here.

After slashings are published and included in blocks, we can examine the situation when our validator is slashed.
Validator slashed
Slashings are divided into two phases: the immediate slashing and the deferred slashing. The purpose of this scheme is to impose a minor penalty on the validator for a single act of misbehavior ("the initial penalty"). However, if multiple validators collaborate to finalize a malicious version of the chain and the total effective balance involved becomes significant, the secondary slashing imposes a much harsher penalty ("the correlation penalty"). A more detailed explanation of the economic rationale behind slashing can be found in the documentation, mentioned above (here).

Slashing actions, such as removing a slashed validator, must be performed immediately in the earliest block where slashing is applicable. This immediate phase("initial penalty") is handled in the "per-block" processing functions process_proposer_slashings() and process_attester_slashings(). After verifying a proposer or attester slashing message (by validating conflicting block hashes and signatures), the slash_validator() function is called. This function immediately initiates the validator's exit (reviewed earlier in the "exit" section). In the case of slashing, however, the earliest withdrawable_epoch is set to EPOCHS_PER_SLASHINGS_VECTOR (8192 epochs) — a large time window to accommodate slashings during large-scale validator misbehavior. This means the slashed validator can only withdraw after approximately 36 days.

After the slashed validator's exit is initiated, its balance is reduced, and the overall state's effective balance changes are updated. The final step is a payment to the whistleblower who reported the slashed validator (as described in an earlier section).

The second("correlation penalty") phase of slashing is processed "per-epoch" in the process_slashings() function. During this phase, all slashings accumulated in the beacon state are summarized, and the adjusted slashing balance is calculated. This balance, along with the total validator balance, is used to determine the fraction of validators' balances to be slashed. The adjusted balance may be increased using the multiplier spec.proportional_slashing_multiplier_for_state (3(Bellatrix+)) to make slashings more punitive.

Finally, slashed validators "targeted" for the current epoch are selected. The effective balances of each slashed validator are then reduced proportionally to adjusted_total_slashing_balance / total_balance.
Validator withdrawing
The withdrawals for validators (enabled after Capella hardfork) require withdrawal credentials. Withdrawal credentials are separate from the validator's signing key. Our validator is managed using two key pairs: signing and withdrawal. The signing key is primarily used for signing blocks and attestations, while the withdrawal key is exclusively used for withdrawals.

Initially, withdrawal credentials contained a hash of the BLS public key. However, after the Capella hardfork made ETH withdrawals possible, it became an option to use Eth1 credentials instead. Now, staking-deposit-cli can use the same mnemonic phrase to generate both signing and withdrawal BLS keys or use a standard Eth1 address for withdrawals. As discussed in the "activating" section, these credentials are included in the Deposit message data and stored for each validator.

Withdrawal credentials can be updated using the signing key through a special message processed by the process_bls_to_execution_changes() function during "per-block" processing.

A validator can withdraw their balance by creating a special withdrawal request. This request can only be executed after spec.min_validator_withdrawability_delay (256) epochs in regular case or after EpochsPerSlashingsVector (8192) epochs in case when the validator was slashed. The processing of withdrawals, similar to deposits, involves both beacon and execution layers because this process "transfers" the validator's "beacon balance" to their "ETH balance."

The expected withdrawals for the current beacon block are generated by the get_expected_withdrawals() function. This function processes a maximum of spec.max_validators_per_withdrawals_sweep (16384) validators per block. If the limit is reached, the process saves information to continue withdrawals in subsequent blocks. Full balances are withdrawn for "fully withdrawable" validators, while only the excess balance above the "working" effective balance is withdrawn for those in a "partially withdrawable" status.

Both scenarios populate the withdrawals list, which is then passed to the execution layer to form Eth1 blocks, where the actual ETH balances changes occur.
Validator in sync committee
The sync committee is an additional committee with a target size of SyncCommitteeSize (512) validators, which is reshuffled from all active validators every 16384 blocks (256 epochs, approximately 27.3 hours). The purpose of the sync committee is to provide a "lighter" version of verifiable information about the beacon chain head for light clients. This allows light clients to avoid extensive verification checks, as the sync committee is updated infrequently and its public keys are stored directly in the beacon state. A light client only needs the N=SyncCommitteeSize public keys and a single aggregated BLS signature to verify each new block.

The annotated specification of the sync protocol is available here, which includes the LightClientSnapshot structure representing the current beacon block header along with the current and next sync committees.

The generation of the next sync committee's validator list is demonstrated here. It uses the same shuffling process and seed as all other committees, weighted by the validators' effective balances.

The output of the sync committee is processed "per-block" in the process_sync_aggregate() function. Here, we verify the aggregate signature (created here), calculate rewards for participating members and the block proposer, and reward every participant and the block proposer, including scenarios where the proposer also participates in the sync committee.

You can calculate the probability of being selected for the sync committee based on the total number of active validators (~10^6 today), SyncCommitteeSize (512), and the 256-epoch (~27.3 hours) period. Although the chance of selection is low, the rewards for participating in the sync committee are substantial.
Chain specification point of view
Let's create another perspective, a "chain-parameters-based" view of validator statuses, offering readers an additional understanding of their lifecycle. Below are some significant parameters and getter function names related to validator statuses and their values. These names are frequently found in various locations, such as chain_spec.rs, eth_spec.rs, or other configuration files. Sometimes, they appear as getter functions, often tailored to specific hardfork versions (like here). Here we’ll use informative names that can be searched in the code and provide values relevant to different hardforks. Let’s begin:
ACTIVATING
max_per_epoch_activation_churn_limit (8(Phase0) -> 256(Electra)) - maximum number of validators that can be activated per epoch.
min_deposit_amount (1 ETH) - the minimum deposit amount.
min_activation_balance (32 ETH) - the minimum balance required to activate a validator.
max_seed_lookahead (4 epochs) - parameter for the "delayed epoch," which regulates the delay for validator's activation
0x00000000219ab540356cBB839Cbe05303d7705Fa - address of the Eth1 deposit contract.
EXITING
min_per_epoch_churn_limit (4(Phase0) -> 128(Electra)) and churn_limit_quotient(65536) - parameters that regulate the maximum "per-epoch" amount of exiting stake.
max_seed_lookahead (4 epochs) - the same "delayed epoch" parameter from the "ACTIVATION" part, but here, it determines the earliest epoch for validator exit.
shard_committee_period (256 epochs) - duration of validator participation in the shard committee, during which they cannot exit.
ACTIVE
max_committees_per_slot (64) - number of validator committees per single slot.
target_committee_size (128) - the "minimal" number of validators in a committee (though this can be more complex).
max_effective_balance (32 ETH(Phase0) -> 2048 ETH(Electra)) - the maximum effective balance of a validator.
ejection_balance (16 ETH) - effective balance below this threshold leads to automatic ejection (exit) of the validator.
effective_balance_increment (1 ETH) - a "measurement unit" for effective balances, used in calculations for rewards and slashing. All effective balances are multiples of this value, and all small movements of the active balance are "tied" to multiples of this value.
hysteresis_downward_multiplier, hysteresis_upward_multiplier, and hysteresis_quotient - a set of parameters defining the hysteresis of effective balances.
shard_committee_period (256 epochs) - duration of validator presence in the shard committee (cannot exit earlier).
PROPOSING BLOCKS
seconds_per_slot (12 sec) - the duration of a single slot in seconds.
base_rewards_per_epoch (4) and base_reward_factor (64) - parameters that influence the base reward for the proposing validator, in conjunction with the square root of total effective balance.
PROPOSER_WEIGHT (8) (8/64 of WEIGHT_DENOMINATOR (64)) - parameter defining the fraction (8/64) of block base rewards paid to the proposer for block proposal (additional rewards are provided for including attestations, slashing evidence, etc.).
ATTESTING BLOCKS
base_rewards_per_epoch (4) and base_reward_factor (64) - parameters that influence the base reward for the attesting validator, along with the square root of total effective balance.
min_attestation_inclusion_delay (1) - the minimum number of slots that must pass before the attestation can be included in a block.
TIMELY_SOURCE_WEIGHT (14), TIMELY_TARGET_WEIGHT (26), TIMELY_HEAD_WEIGHT (14), SYNC_REWARD_WEIGHT(2) and WEIGHT_DENOMINATOR (64) - fractions of rewards for attestations
DOING NOTHING
min_epochs_to_inactivity_penalty (4 epochs) - chain finality delay before the "inactivity leak" begins, penalizing validators for inactivity.
inactivity_penalty_quotient (2^24 epochs) - parameter involved in the calculation finality_delay/inactivity_penalty_quotient, which determines the fraction of a validator's effective balance penalized for inactivity.
inactivity_score_recovery_rate (16) - "per-epoch" rate of forgiveness for validators when the "inactivity leak" is over. This parameter reduces the validator’s inactivity score.
inactivity_score_bias (4) - an additional parameter regulating the "force" of inactivity scoring, which is used to calculate inactivity penalties.
SLASHING OTHERS
whistleblower_reward_quotient (512(Phase0) -> 4096(Electra)) - fraction of the slashed validator's effective balance awarded to the whistleblower who provided proof of slashing.
SLASHED BY OTHERS
min_slashing_penalty_quotient (64(Altair) -> 32(Bellatrix) -> 2048(Electra)) - fraction of a validator's effective balance slashed immediately (with an additional deferred component in mass slashing scenarios).
proportional_slashing_multiplier (2(Altair) -> 3(Bellatrix)) - multiplier applied to total_slashing_balance to increase slashing severity.
WITHDRAWING
min_validator_withdrawability_delay (256 epochs) - delay before a validator can withdraw.
max_validators_per_withdrawals_sweep (16384) - maximum number of withdrawals allowed in a single block, passed to the Eth1 execution layer.
max_pending_partials_per_withdrawals_sweep (4) - parameter related to pending withdrawals, as detailed in EIP7251.
Of course, these are not all the parameters used in Ethereum consensus, but we hope this view helps the reader reinforce their knowledge about the validator's lifecycle.
Conclusion
The Ethereum validator lifecycle is complex. Activation, exits, participation, and slashing all require meticulous attention in a decentralized environment. Furthermore, real-world implementations must efficiently perform numerous operations, with validator node performance being crucial to the network’s success.

Studying the chain specification and validator implementation source code individually can be challenging. That’s why we strive to connect documentation with implementation elements in this article, aiming to provide a clearer understanding of what occurs "under the hood" in production-ready code. We hope this content is valuable to blockchain developers and security specialists.

Thank you for reading, and stay tuned for our next articles!
  • Who is MixBytes?
    MixBytes is a team of expert blockchain auditors and security researchers specializing in providing comprehensive smart contract audits and technical advisory services for EVM-compatible and Substrate-based projects. Join us on X to stay up-to-date with the latest industry trends and insights.
  • Disclaimer
    The information contained in this Website is for educational and informational purposes only and shall not be understood or construed as financial or investment advice.
Other posts