eth2.0-specs/specs/merge/fork-choice.md

7.9 KiB

The Merge -- Fork Choice

Notice: This document is a work-in-progress for researchers and implementers.

Table of contents

Introduction

This is the modification of the fork choice according to the executable beacon chain proposal.

Note: It introduces the process of transition from the last PoW block to the first PoS block.

Protocols

ExecutionEngine

Note: The notify_forkchoice_updated function is added to the ExecutionEngine protocol to signal the fork choice updates.

The body of this function is implementation dependent. The Engine API may be used to implement it with an external execution engine.

notify_forkchoice_updated

This function performs two actions atomically:

  • Re-organizes the execution payload chain and corresponding state to make head_block_hash the head.
  • Applies finality to the execution state: it irreversibly persists the chain of all execution payloads and corresponding state, up to and including finalized_block_hash.

Additionally, if payload_attributes is provided, this function sets in motion a payload build process on top of head_block_hash with the result to be gathered by a followup call to get_payload.

def notify_forkchoice_updated(self: ExecutionEngine,
                              head_block_hash: Hash32,
                              finalized_block_hash: Hash32,
                              payload_attributes: Optional[PayloadAttributes]) -> None:
    ...

Note: The call of the notify_forkchoice_updated function maps on the POS_FORKCHOICE_UPDATED event defined in the EIP-3675. As per EIP-3675, before a post-transition block is finalized, notify_forkchoice_updated must be called with finalized_block_hash = Hash32().

Helpers

PayloadAttributes

Used to signal to initiate the payload build process via notify_forkchoice_updated.

@dataclass
class PayloadAttributes(object):
    timestamp: uint64
    random: Bytes32
    fee_recipient: ExecutionAddress

PowBlock

class PowBlock(Container):
    block_hash: Hash32
    parent_hash: Hash32
    total_difficulty: uint256
    difficulty: uint256

get_pow_block

Let get_pow_block(block_hash: Hash32) -> Optional[PowBlock] be the function that given the hash of the PoW block returns its data. It may result in None if the requested block is not yet available.

Note: The eth_getBlockByHash JSON-RPC method may be used to pull this information from an execution client.

is_valid_terminal_pow_block

Used by fork-choice handler, on_block.

def is_valid_terminal_pow_block(block: PowBlock, parent: PowBlock) -> bool:
    if TERMINAL_BLOCK_HASH != Hash32():
        return block.block_hash == TERMINAL_BLOCK_HASH

    is_total_difficulty_reached = block.total_difficulty >= TERMINAL_TOTAL_DIFFICULTY
    is_parent_total_difficulty_valid = parent.total_difficulty < TERMINAL_TOTAL_DIFFICULTY
    return is_total_difficulty_reached and is_parent_total_difficulty_valid

validate_merge_block

def validate_merge_block(block: BeaconBlock) -> None:
    """
    Check the parent PoW block of execution payload is a valid terminal PoW block.

    Note: Unavailable PoW block(s) may later become available,
    and a client software MAY delay a call to ``validate_merge_block``
    until the PoW block(s) become available.
    """
    pow_block = get_pow_block(block.body.execution_payload.parent_hash)
    # Check if `pow_block` is available
    assert pow_block is not None
    pow_parent = get_pow_block(pow_block.parent_hash)
    # Check if `pow_parent` is available
    assert pow_parent is not None
    # Check if `pow_block` is a valid terminal PoW block
    assert is_valid_terminal_pow_block(pow_block, pow_parent)

    # If `TERMINAL_BLOCK_HASH` is used as an override, the activation epoch must be reached.
    if TERMINAL_BLOCK_HASH != Hash32():
        assert compute_epoch_at_slot(block.slot) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH

Updated fork-choice handlers

on_block

Note: The only modification is the addition of the verification of transition block conditions.

def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
    """
    Run ``on_block`` upon receiving a new block.

    A block that is asserted as invalid due to unavailable PoW block may be valid at a later time,
    consider scheduling it for later processing in such case.
    """
    block = signed_block.message
    # Parent block must be known
    assert block.parent_root in store.block_states
    # Make a copy of the state to avoid mutability issues
    pre_state = copy(store.block_states[block.parent_root])
    # Blocks cannot be in the future. If they are, their consideration must be delayed until the are in the past.
    assert get_current_slot(store) >= block.slot

    # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor)
    finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
    assert block.slot > finalized_slot
    # Check block is a descendant of the finalized block at the checkpoint finalized slot
    assert get_ancestor(store, block.parent_root, finalized_slot) == store.finalized_checkpoint.root

    # Check the block is valid and compute the post-state
    state = pre_state.copy()
    state_transition(state, signed_block, True)

    # [New in Merge]
    if is_merge_block(pre_state, block.body):
        validate_merge_block(block)

    # Add new block to the store
    store.blocks[hash_tree_root(block)] = block
    # Add new state for this block to the store
    store.block_states[hash_tree_root(block)] = state

    # Update justified checkpoint
    if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
        if state.current_justified_checkpoint.epoch > store.best_justified_checkpoint.epoch:
            store.best_justified_checkpoint = state.current_justified_checkpoint
        if should_update_justified_checkpoint(store, state.current_justified_checkpoint):
            store.justified_checkpoint = state.current_justified_checkpoint

    # Update finalized checkpoint
    if state.finalized_checkpoint.epoch > store.finalized_checkpoint.epoch:
        store.finalized_checkpoint = state.finalized_checkpoint

        # Potentially update justified if different from store
        if store.justified_checkpoint != state.current_justified_checkpoint:
            # Update justified if new justified is later than store justified
            if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
                store.justified_checkpoint = state.current_justified_checkpoint
                return

            # Update justified if store justified is not in chain with finalized checkpoint
            finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
            ancestor_at_finalized_slot = get_ancestor(store, store.justified_checkpoint.root, finalized_slot)
            if ancestor_at_finalized_slot != store.finalized_checkpoint.root:
                store.justified_checkpoint = state.current_justified_checkpoint