eth2.0-specs/specs/validator/0_beacon-chain-validator.md

25 KiB

Ethereum 2.0 Phase 0 -- Honest Validator

Notice: This document is a work-in-progress for researchers and implementers. This is an accompanying document to Ethereum 2.0 Phase 0 -- The Beacon Chain, which describes the expected actions of a "validator" participating in the Ethereum 2.0 protocol.

Table of contents

Introduction

This document represents the expected behavior of an "honest validator" with respect to Phase 0 of the Ethereum 2.0 protocol. This document does not distinguish between a "node" (i.e. the functionality of following and reading the beacon chain) and a "validator client" (i.e. the functionality of actively participating in consensus). The separation of concerns between these (potentially) two pieces of software is left as a design decision that is out of scope.

A validator is an entity that participates in the consensus of the Ethereum 2.0 protocol. This is an optional role for users in which they can post ETH as collateral and verify and attest to the validity of blocks to seek financial returns in exchange for building and securing the protocol. This is similar to proof-of-work networks in which miners provide collateral in the form of hardware/hash-power to seek returns in exchange for building and securing the protocol.

Prerequisites

All terminology, constants, functions, and protocol mechanics defined in the Phase 0 -- The Beacon Chain and Phase 0 -- Deposit Contract doc are requisite for this document and used throughout. Please see the Phase 0 doc before continuing and use as a reference throughout.

Constants

Misc

Name Value Unit Duration
ETH1_FOLLOW_DISTANCE 2**10 (= 1,024) blocks ~4 hours

Becoming a validator

Initialization

A validator must initialize many parameters locally before submitting a deposit and joining the validator registry.

BLS public key

Validator public keys are G1 points on the BLS12-381 curve. A private key, privkey, must be securely generated along with the resultant pubkey. This privkey must be "hot", that is, constantly available to sign data throughout the lifetime of the validator.

BLS withdrawal key

A secondary withdrawal private key, withdrawal_privkey, must also be securely generated along with the resultant withdrawal_pubkey. This withdrawal_privkey does not have to be available for signing during the normal lifetime of a validator and can live in "cold storage".

The validator constructs their withdrawal_credentials via the following:

  • Set withdrawal_credentials[:1] == BLS_WITHDRAWAL_PREFIX_BYTE.
  • Set withdrawal_credentials[1:] == hash(withdrawal_pubkey)[1:].

Submit deposit

In Phase 0, all incoming validator deposits originate from the Ethereum 1.0 proof-of-work chain. Deposits are made to the deposit contract located at DEPOSIT_CONTRACT_ADDRESS.

To submit a deposit:

  • Pack the validator's initialization parameters into deposit_data, a DepositData SSZ object.
  • Let amount be the amount in Gwei to be deposited by the validator where MIN_DEPOSIT_AMOUNT <= amount <= MAX_EFFECTIVE_BALANCE.
  • Set deposit_data.amount = amount.
  • Let signature be the result of bls_sign of the signing_root(deposit_data) with domain=bls_domain(DOMAIN_DEPOSIT). (Deposits are valid regardless of fork version, bls_domain will default to zeroes there).
  • Send a transaction on the Ethereum 1.0 chain to DEPOSIT_CONTRACT_ADDRESS executing def deposit(pubkey: bytes[48], withdrawal_credentials: bytes[32], signature: bytes[96]) along with a deposit of amount Gwei.

Note: Deposits made for the same pubkey are treated as for the same validator. A singular Validator will be added to state.validators with each additional deposit amount added to the validator's balance. A validator can only be activated when total deposits for the validator pubkey meet or exceed MAX_EFFECTIVE_BALANCE.

Process deposit

Deposits cannot be processed into the beacon chain until the Eth 1.0 block in which they were deposited or any of its descendants is added to the beacon chain state.eth1_data. This takes a minimum of ETH1_FOLLOW_DISTANCE Eth 1.0 blocks (~4 hours) plus ETH1_DATA_VOTING_PERIOD epochs (~1.7 hours). Once the requisite Eth 1.0 data is added, the deposit will normally be added to a beacon chain block and processed into the state.validators within an epoch or two. The validator is then in a queue to be activated.

Validator index

Once a validator has been processed and added to the beacon state's validators, the validator's validator_index is defined by the index into the registry at which the ValidatorRecord contains the pubkey specified in the validator's deposit. A validator's validator_index is guaranteed to not change from the time of initial deposit until the validator exits and fully withdraws. This validator_index is used throughout the specification to dictate validator roles and responsibilities at any point and should be stored locally.

Activation

In normal operation, the validator is quickly activated, at which point the validator is added to the shuffling and begins validation after an additional ACTIVATION_EXIT_DELAY epochs (25.6 minutes).

The function is_active_validator can be used to check if a validator is active during a given epoch. Usage is as follows:

def check_if_validator_active(state: BeaconState, validator_index: ValidatorIndex) -> bool:
    validator = state.validators[validator_index]
    return is_active_validator(validator, get_current_epoch(state))

Once a validator is activated, the validator is assigned responsibilities until exited.

Note: There is a maximum validator churn per finalized epoch, so the delay until activation is variable depending upon finality, total active validator balance, and the number of validators in the queue to be activated.

Validator assignments

A validator can get committee assignments for a given epoch using the following helper via get_committee_assignment(state, epoch, validator_index) where epoch <= next_epoch.

def get_committee_assignment(state: BeaconState,
                             epoch: Epoch,
                             validator_index: ValidatorIndex) -> Tuple[Sequence[ValidatorIndex], Shard, Slot]:
    """
    Return the committee assignment in the ``epoch`` for ``validator_index``.
    ``assignment`` returned is a tuple of the following form:
        * ``assignment[0]`` is the list of validators in the committee
        * ``assignment[1]`` is the shard to which the committee is assigned
        * ``assignment[2]`` is the slot at which the committee is assigned
    """
    next_epoch = get_current_epoch(state) + 1
    assert epoch <= next_epoch

    committees_per_slot = get_committee_count(state, epoch) // SLOTS_PER_EPOCH
    start_slot = epoch_start_slot(epoch)
    for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH):
        offset = committees_per_slot * (slot % SLOTS_PER_EPOCH)
        slot_start_shard = (get_start_shard(state, epoch) + offset) % SHARD_COUNT
        for i in range(committees_per_slot):
            shard = Shard((slot_start_shard + i) % SHARD_COUNT)
            committee = get_crosslink_committee(state, epoch, shard)
            if validator_index in committee:
                break
        else:
            continue
        break
    return committee, shard, Slot(slot)

A validator can use the following function to see if they are supposed to propose during their assigned committee slot. This function can only be run with a state of the slot in question. Proposer selection is only stable within the context of the current epoch.

def is_proposer(state: BeaconState,
                validator_index: ValidatorIndex) -> bool:
    return get_beacon_proposer_index(state) == validator_index

Note: To see if a validator is assigned to propose during the slot, the beacon state must be in the epoch in question. At the epoch boundaries, the validator must run an epoch transition into the epoch to successfully check the proposal assignment of the first slot.

Lookahead

The beacon chain shufflings are designed to provide a minimum of 1 epoch lookahead on the validator's upcoming committee assignments for attesting dictated by the shuffling and slot. Note that this lookahead does not apply to proposing, which must be checked during the epoch in question.

get_committee_assignment should be called at the start of each epoch to get the assignment for the next epoch (current_epoch + 1). A validator should plan for future assignments by noting at which future slot they will have to attest and also which shard they should begin syncing (in Phase 1+).

Specifically, a validator should call get_committee_assignment(state, next_epoch, validator_index) when checking for next epoch assignments.

Beacon chain responsibilities

A validator has two primary responsibilities to the beacon chain: proposing blocks and creating attestations. Proposals happen infrequently, whereas attestations should be created once per epoch.

Block proposal

A validator is expected to propose a BeaconBlock at the beginning of any slot during which is_proposer(state, validator_index) returns True. To propose, the validator selects the BeaconBlock, parent, that in their view of the fork choice is the head of the chain during slot - 1. The validator creates, signs, and broadcasts a block that is a child of parent that satisfies a valid beacon chain state transition.

There is one proposer per slot, so if there are N active validators any individual validator will on average be assigned to propose once per N slots (e.g. at 312,500 validators = 10 million ETH, that's once per ~3 weeks).

Block header

Slot

Set block.slot = slot where slot is the current slot at which the validator has been selected to propose. The parent selected must satisfy that parent.slot < block.slot.

Note: There might be "skipped" slots between the parent and block. These skipped slots are processed in the state transition function without per-block processing.

Parent root

Set block.parent_root = signing_root(parent).

State root

Set block.state_root = hash_tree_root(state) of the resulting state of the parent -> block state transition.

Note: To calculate state_root, the validator should first run the state transition function on an unsigned block containing a stub for the state_root. It is useful to be able to run a state transition function that does not validate signatures or state root for this purpose.

Randao reveal

Set block.randao_reveal = epoch_signature where epoch_signature is obtained from:

def get_epoch_signature(state: BeaconState, block: BeaconBlock, privkey: int) -> BLSSignature:
    return bls_sign(
        privkey=privkey,  # privkey stored locally
        message_hash=hash_tree_root(slot_to_epoch(block.slot)),
        domain=get_domain(
            state=state,
            domain_type=DOMAIN_RANDAO,
            message_epoch=slot_to_epoch(block.slot),
        )
    )
Eth1 Data

The block.eth1_data field is for block proposers to vote on recent Eth 1.0 data. This recent data contains an Eth 1.0 block hash as well as the associated deposit root (as calculated by the get_hash_tree_root() method of the deposit contract) and deposit count after execution of the corresponding Eth 1.0 block. If over half of the block proposers in the current Eth 1.0 voting period vote for the same eth1_data then state.eth1_data updates at the end of the voting period. Each deposit in block.body.deposits must verify against state.eth1_data.eth1_deposit_root.

Let get_eth1_data(distance: uint64) -> Eth1Data be the (subjective) function that returns the Eth 1.0 data at distance distance relative to the Eth 1.0 head at the start of the current Eth 1.0 voting period. Let previous_eth1_distance be the distance relative to the Eth 1.0 block corresponding to state.eth1_data.block_hash at the start of the current Eth 1.0 voting period. An honest block proposer sets block.eth1_data = get_eth1_vote(state, previous_eth1_distance) where:

def get_eth1_vote(state: BeaconState, previous_eth1_distance: uint64) -> Eth1Data:
    new_eth1_data = [get_eth1_data(distance) for distance in range(ETH1_FOLLOW_DISTANCE, 2 * ETH1_FOLLOW_DISTANCE)]
    all_eth1_data = [get_eth1_data(distance) for distance in range(ETH1_FOLLOW_DISTANCE, previous_eth1_distance)]

    valid_votes = []
    for slot, vote in enumerate(state.eth1_data_votes):
        period_tail = slot % SLOTS_PER_ETH1_VOTING_PERIOD >= integer_squareroot(SLOTS_PER_ETH1_VOTING_PERIOD)
        if vote in new_eth1_data or (period_tail and vote in all_eth1_data):
            valid_votes.append(vote)

    return max(
        valid_votes,
        key=lambda v: (valid_votes.count(v), -all_eth1_data.index(v)),  # Tiebreak by smallest distance
        default=get_eth1_data(ETH1_FOLLOW_DISTANCE),
    )
Signature

Set header.signature = block_signature where block_signature is obtained from:

def get_block_signature(state: BeaconState, header: BeaconBlockHeader, privkey: int) -> BLSSignature:
    return bls_sign(
        privkey=privkey,  # privkey stored locally
        message_hash=signing_root(header),
        domain=get_domain(
            state=state,
            domain_type=DOMAIN_BEACON_PROPOSER,
            message_epoch=slot_to_epoch(header.slot),
        )
    )

Block body

Proposer slashings

Up to MAX_PROPOSER_SLASHINGS, ProposerSlashing objects can be included in the block. The proposer slashings must satisfy the verification conditions found in proposer slashings processing. The validator receives a small "whistleblower" reward for each proposer slashing found and included.

Attester slashings

Up to MAX_ATTESTER_SLASHINGS, AttesterSlashing objects can be included in the block. The attester slashings must satisfy the verification conditions found in attester slashings processing. The validator receives a small "whistleblower" reward for each attester slashing found and included.

Attestations

Up to MAX_ATTESTATIONS, aggregate attestations can be included in the block. The attestations added must satisfy the verification conditions found in attestation processing. To maximize profit, the validator should attempt to gather aggregate attestations that include singular attestations from the largest number of validators whose signatures from the same epoch have not previously been added on chain.

Deposits

If there are any unprocessed deposits for the existing state.eth1_data (i.e. state.eth1_data.deposit_count > state.eth1_deposit_index), then pending deposits must be added to the block. The expected number of deposits is exactly min(MAX_DEPOSITS, eth1_data.deposit_count - state.eth1_deposit_index). These deposits are constructed from the Deposit logs from the Eth 1.0 deposit contract and must be processed in sequential order. The deposits included in the block must satisfy the verification conditions found in deposits processing.

The proof for each deposit must be constructed against the deposit root contained in state.eth1_data rather than the deposit root at the time the deposit was initially logged from the 1.0 chain. This entails storing a full deposit merkle tree locally and computing updated proofs against the eth1_data.deposit_root as needed. See minimal_merkle.py for a sample implementation.

Voluntary exits

Up to MAX_VOLUNTARY_EXITS, VoluntaryExit objects can be included in the block. The exits must satisfy the verification conditions found in exits processing.

Attestations

A validator is expected to create, sign, and broadcast an attestation during each epoch. The committee, assigned shard, and assigned slot for which the validator performs this role during an epoch are defined by get_committee_assignment(state, epoch, validator_index).

A validator should create and broadcast the attestation halfway through the slot during which the validator is assigned―that is, SECONDS_PER_SLOT * 0.5 seconds after the start of slot.

Attestation data

First, the validator should construct attestation_data, an AttestationData object based upon the state at the assigned slot.

  • Let head_block be the result of running the fork choice during the assigned slot.
  • Let head_state be the state of head_block processed through any empty slots up to the assigned slot using process_slots(state, slot).
LMD GHOST vote

Set attestation_data.beacon_block_root = signing_root(head_block).

FFG vote
  • Set attestation_data.source_epoch = head_state.current_justified_epoch.
  • Set attestation_data.source_root = head_state.current_justified_root.
  • Set attestation_data.target_epoch = get_current_epoch(head_state)
  • Set attestation_data.target_root = epoch_boundary_block_root where epoch_boundary_block_root is the root of block at the most recent epoch boundary.

Note: epoch_boundary_block_root can be looked up in the state using:

  • Let start_slot = epoch_start_slot(get_current_epoch(head_state)).
  • Let epoch_boundary_block_root = signing_root(head_block) if start_slot == head_state.slot else get_block_root(state, start_slot).

Construct attestation_data.crosslink via the following.

  • Set attestation_data.crosslink.shard = shard where shard is the shard associated with the validator's committee.
  • Let parent_crosslink = head_state.current_crosslinks[shard].
  • Set attestation_data.crosslink.start_epoch = parent_crosslink.end_epoch.
  • Set attestation_data.crosslink.end_epoch = min(attestation_data.target_epoch, parent_crosslink.end_epoch + MAX_EPOCHS_PER_CROSSLINK).
  • Set attestation_data.crosslink.parent_root = hash_tree_root(head_state.current_crosslinks[shard]).
  • Set attestation_data.crosslink.data_root = ZERO_HASH. Note: This is a stub for Phase 0.

Construct attestation

Next, the validator creates attestation, an Attestation object.

Data

Set attestation.data = attestation_data where attestation_data is the AttestationData object defined in the previous section, attestation data.

Aggregation bits
  • Let attestation.aggregation_bits be a Bitlist[MAX_INDICES_PER_ATTESTATION] where the bits at the index in the aggregated validator's committee is set to 0b1.

Note: Calling get_attesting_indices(state, attestation.data, attestation.aggregation_bits) should return a list of length equal to 1, containing validator_index.

Custody bits
  • Let attestation.custody_bits be a Bitlist[MAX_INDICES_PER_ATTESTATION] filled with zeros of length len(committee).

Note: This is a stub for Phase 0.

Aggregate signature

Set attestation.signature = signed_attestation_data where signed_attestation_data is obtained from:

def get_signed_attestation_data(state: BeaconState, attestation: IndexedAttestation, privkey: int) -> BLSSignature:
    attestation_data_and_custody_bit = AttestationDataAndCustodyBit(
        data=attestation.data,
        custody_bit=0b0,
    )

    return bls_sign(
        privkey=privkey,  # privkey stored locally
        message_hash=hash_tree_root(attestation_data_and_custody_bit),
        domain=get_domain(
            state=state,
            domain_type=DOMAIN_ATTESTATION,
            message_epoch=attestation.data.target.epoch,
        )
    )

How to avoid slashing

"Slashing" is the burning of some amount of validator funds and immediate ejection from the active validator set. In Phase 0, there are two ways in which funds can be slashed: proposer slashing and attester slashing. Although being slashed has serious repercussions, it is simple enough to avoid being slashed all together by remaining consistent with respect to the messages a validator has previously signed.

Note: Signed data must be within a sequential Fork context to conflict. Messages cannot be slashed across diverging forks. If the previous fork version is 1 and the chain splits into fork 2 and 102, messages from 1 can slashable against messages in forks 1, 2, and 102. Messages in 2 cannot be slashable against messages in 102, and vice versa.

Proposer slashing

To avoid "proposer slashings", a validator must not sign two conflicting BeaconBlock where conflicting is defined as two distinct blocks within the same epoch.

In Phase 0, as long as the validator does not sign two different beacon blocks for the same epoch, the validator is safe against proposer slashings.

Specifically, when signing a BeaconBlock, a validator should perform the following steps in the following order:

  1. Save a record to hard disk that a beacon block has been signed for the epoch=slot_to_epoch(block.slot).
  2. Generate and broadcast the block.

If the software crashes at some point within this routine, then when the validator comes back online, the hard disk has the record of the potentially signed/broadcast block and can effectively avoid slashing.

Attester slashing

To avoid "attester slashings", a validator must not sign two conflicting AttestationData objects, i.e. two attestations that satisfy is_slashable_attestation_data.

Specifically, when signing an Attestation, a validator should perform the following steps in the following order:

  1. Save a record to hard disk that an attestation has been signed for source (i.e. attestation_data.source_epoch) and target (i.e. slot_to_epoch(attestation_data.slot)).
  2. Generate and broadcast attestation.

If the software crashes at some point within this routine, then when the validator comes back online, the hard disk has the record of the potentially signed/broadcast attestation and can effectively avoid slashing.