Skip to content

Fulu -- The Beacon Chain

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

Introduction

Note: This specification is built upon Electra and is under active development.

Configuration

Blob schedule

[New in Fulu:EIP7892] This schedule defines the maximum blobs per block limit for a given epoch.

There MUST NOT exist multiple blob schedule entries with the same epoch value. The maximum blobs per block limit for blob schedules entries MUST be less than or equal to MAX_BLOB_COMMITMENTS_PER_BLOCK. The blob schedule entries SHOULD be sorted by epoch in ascending order. The blob schedule MAY be empty.

Note: The blob schedule is to be determined.

Epoch Max Blobs Per Block Description

Beacon chain state transition function

Block processing

Execution payload

Modified process_execution_payload
def process_execution_payload(
    state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine
) -> None:
    payload = body.execution_payload

    # Verify consistency of the parent hash with respect to the previous execution payload header
    assert payload.parent_hash == state.latest_execution_payload_header.block_hash
    # Verify prev_randao
    assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state))
    # Verify timestamp
    assert payload.timestamp == compute_timestamp_at_slot(state, state.slot)
    # [Modified in Fulu:EIP7892] Verify commitments are under limit
    assert (
        len(body.blob_kzg_commitments)
        <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block
    )
    # Verify the execution payload is valid
    versioned_hashes = [
        kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments
    ]
    assert execution_engine.verify_and_notify_new_payload(
        NewPayloadRequest(
            execution_payload=payload,
            versioned_hashes=versioned_hashes,
            parent_beacon_block_root=state.latest_block_header.parent_root,
            execution_requests=body.execution_requests,
        )
    )
    # Cache execution payload header
    state.latest_execution_payload_header = ExecutionPayloadHeader(
        parent_hash=payload.parent_hash,
        fee_recipient=payload.fee_recipient,
        state_root=payload.state_root,
        receipts_root=payload.receipts_root,
        logs_bloom=payload.logs_bloom,
        prev_randao=payload.prev_randao,
        block_number=payload.block_number,
        gas_limit=payload.gas_limit,
        gas_used=payload.gas_used,
        timestamp=payload.timestamp,
        extra_data=payload.extra_data,
        base_fee_per_gas=payload.base_fee_per_gas,
        block_hash=payload.block_hash,
        transactions_root=hash_tree_root(payload.transactions),
        withdrawals_root=hash_tree_root(payload.withdrawals),
        blob_gas_used=payload.blob_gas_used,
        excess_blob_gas=payload.excess_blob_gas,
    )

Containers

Extended Containers

BeaconState

Note: The BeaconState container is extended with the proposer_lookahead field, which is a list of validator indices covering the full lookahead period, starting from the beginning of the current epoch. For example, proposer_lookahead[0] is the validator index for the first proposer in the current epoch, proposer_lookahead[1] is the validator index for the next proposer in the current epoch, and so forth. The length of the proposer_lookahead list is (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH, reflecting how far ahead proposer indices are computed based on the MIN_SEED_LOOKAHEAD parameter.

class BeaconState(Container):
    genesis_time: uint64
    genesis_validators_root: Root
    slot: Slot
    fork: Fork
    latest_block_header: BeaconBlockHeader
    block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
    state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
    historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT]
    eth1_data: Eth1Data
    eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
    eth1_deposit_index: uint64
    validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
    balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
    randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]
    slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR]
    previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
    current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
    justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH]
    previous_justified_checkpoint: Checkpoint
    current_justified_checkpoint: Checkpoint
    finalized_checkpoint: Checkpoint
    inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT]
    current_sync_committee: SyncCommittee
    next_sync_committee: SyncCommittee
    latest_execution_payload_header: ExecutionPayloadHeader
    next_withdrawal_index: WithdrawalIndex
    next_withdrawal_validator_index: ValidatorIndex
    historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]
    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: List[PendingDeposit, PENDING_DEPOSITS_LIMIT]
    pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]
    pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]
    # [New in Fulu:EIP7917]
    proposer_lookahead: Vector[ValidatorIndex, (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH]

Helper functions

Misc

New BlobParameters

1
2
3
4
@dataclass
class BlobParameters:
    epoch: Epoch
    max_blobs_per_block: uint64

New get_blob_parameters

1
2
3
4
5
6
7
8
def get_blob_parameters(epoch: Epoch) -> BlobParameters:
    """
    Return the blob parameters at a given epoch.
    """
    for entry in sorted(BLOB_SCHEDULE, key=lambda e: e["EPOCH"], reverse=True):
        if epoch >= entry["EPOCH"]:
            return BlobParameters(entry["EPOCH"], entry["MAX_BLOBS_PER_BLOCK"])
    return BlobParameters(ELECTRA_FORK_EPOCH, MAX_BLOBS_PER_BLOCK_ELECTRA)

Modified compute_fork_digest

Note: The compute_fork_digest helper is updated to account for Blob-Parameters-Only forks. Also, the fork_version parameter has been removed and is now computed for the given epoch with compute_fork_version.

def compute_fork_digest(
    genesis_validators_root: Root,
    epoch: Epoch,  # [New in Fulu:EIP7892]
) -> ForkDigest:
    """
    Return the 4-byte fork digest for the ``version`` and ``genesis_validators_root``
    XOR'd with the hash of the blob parameters for ``epoch``.

    This is a digest primarily used for domain separation on the p2p layer.
    4-bytes suffices for practical separation of forks/chains.
    """
    fork_version = compute_fork_version(epoch)
    base_digest = compute_fork_data_root(fork_version, genesis_validators_root)
    blob_parameters = get_blob_parameters(epoch)

    # Bitmask digest with hash of blob parameters
    return ForkDigest(
        bytes(
            xor(
                base_digest,
                hash(
                    uint_to_bytes(uint64(blob_parameters.epoch))
                    + uint_to_bytes(uint64(blob_parameters.max_blobs_per_block))
                ),
            )
        )[:4]
    )

New compute_proposer_indices

1
2
3
4
5
6
7
8
9
def compute_proposer_indices(
    state: BeaconState, epoch: Epoch, seed: Bytes32, indices: Sequence[ValidatorIndex]
) -> Vector[ValidatorIndex, SLOTS_PER_EPOCH]:
    """
    Return the proposer indices for the given ``epoch``.
    """
    start_slot = compute_start_slot_at_epoch(epoch)
    seeds = [hash(seed + uint_to_bytes(Slot(start_slot + i))) for i in range(SLOTS_PER_EPOCH)]
    return [compute_proposer_index(state, indices, seed) for seed in seeds]

Beacon state accessors

Modified get_beacon_proposer_index

Note: The function get_beacon_proposer_index is modified to use the pre-calculated current_proposer_lookahead instead of calculating it on-demand.

1
2
3
4
5
def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex:
    """
    Return the beacon proposer index at the current slot.
    """
    return state.proposer_lookahead[state.slot % SLOTS_PER_EPOCH]

New get_beacon_proposer_indices

1
2
3
4
5
6
7
8
9
def get_beacon_proposer_indices(
    state: BeaconState, epoch: Epoch
) -> Vector[ValidatorIndex, SLOTS_PER_EPOCH]:
    """
    Return the proposer indices for the given ``epoch``.
    """
    indices = get_active_validator_indices(state, epoch)
    seed = get_seed(state, epoch, DOMAIN_BEACON_PROPOSER)
    return compute_proposer_indices(state, epoch, seed, indices)

Epoch processing

Modified process_epoch

Note: The function process_epoch is modified in Fulu to call process_proposer_lookahead to update the proposer_lookahead in the beacon state.

def process_epoch(state: BeaconState) -> None:
    process_justification_and_finalization(state)
    process_inactivity_updates(state)
    process_rewards_and_penalties(state)
    process_registry_updates(state)
    process_slashings(state)
    process_eth1_data_reset(state)
    process_pending_deposits(state)
    process_pending_consolidations(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)  # [New in Fulu:EIP7917]

New process_proposer_lookahead

Note: This function updates the proposer_lookahead field in the beacon state by shifting out proposer indices from the earliest epoch and appending new proposer indices for the latest epoch. With MIN_SEED_LOOKAHEAD set to 1, this means that at the start of epoch N, the proposer lookahead for epoch N+1 will be computed and included in the beacon state's lookahead.

1
2
3
4
5
6
7
8
9
def process_proposer_lookahead(state: BeaconState) -> None:
    last_epoch_start = len(state.proposer_lookahead) - SLOTS_PER_EPOCH
    # Shift out proposers in the first epoch
    state.proposer_lookahead[:last_epoch_start] = state.proposer_lookahead[SLOTS_PER_EPOCH:]
    # Fill in the last epoch with new proposer indices
    last_epoch_proposers = get_beacon_proposer_indices(
        state, Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD + 1)
    )
    state.proposer_lookahead[last_epoch_start:] = last_epoch_proposers