diff --git a/specs/_features/eip7594/fork-choice.md b/specs/_features/eip7594/fork-choice.md index f406b7472a..b1697c75ab 100644 --- a/specs/_features/eip7594/fork-choice.md +++ b/specs/_features/eip7594/fork-choice.md @@ -7,7 +7,11 @@ - [Introduction](#introduction) - [Helpers](#helpers) + - [New `Config`](#new-config) + - [Modified `Store`](#modified-store) - [Modified `is_data_available`](#modified-is_data_available) +- [New fork-choice handlers](#new-fork-choice-handlers) + - [New `on_data_column_sidecar`](#new-on_data_column_sidecar) - [Updated fork-choice handlers](#updated-fork-choice-handlers) - [Modified `on_block`](#modified-on_block) @@ -20,21 +24,114 @@ This is the modification of the fork choice accompanying EIP-7594. ## Helpers +### New `Config` + +*Note*: `Config` is supposed to be pre-populated with configurations specific to each node. + +```python +@dataclass +class Config(object): + metadata: MetaData + node_id: NodeID +``` + +### Modified `Store` + +*Note*: There are two modifications to `Store`: +1. Config is added to keep node-specific configurations. +2. Data column sidecar store is added to keep track the data column sidecars that have been seen. + +```python +@dataclass +class Store(object): + config: Config # [New in EIP7594] + time: uint64 + genesis_time: uint64 + justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + unrealized_justified_checkpoint: Checkpoint + unrealized_finalized_checkpoint: Checkpoint + proposer_boost_root: Root + equivocating_indices: Set[ValidatorIndex] + blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) + block_states: Dict[Root, BeaconState] = field(default_factory=dict) + block_timeliness: Dict[Root, boolean] = field(default_factory=dict) + checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) + latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) + unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) + # blob_sidecars: Dict[BlobIdentifier, BlobSidecar] = field(default_factory=dict) # [Removed in EIP7594] + data_column_sidecars: Dict[DataColumnIdentifier, DataColumnSidecar] = field(default_factory=dict) # [New in EIP7594] +``` + ### Modified `is_data_available` ```python -def is_data_available(beacon_block_root: Root) -> bool: - # `retrieve_column_sidecars` is implementation and context dependent, replacing - # `retrieve_blobs_and_proofs`. For the given block root, it returns all column - # sidecars to sample, or raises an exception if they are not available. - # The p2p network does not guarantee sidecar retrieval outside of - # `MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS` epochs. - column_sidecars = retrieve_column_sidecars(beacon_block_root) +def is_data_available(store: Store, beacon_block_root: Root) -> bool: + # The p2p network does not guarantee sidecar retrieval outside of + # `MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS` epochs. + custody_columns = get_custody_columns(store.config.node_id, store.config.metadata.custody_subnet_count) return all( - verify_data_column_sidecar(column_sidecar) - and verify_data_column_sidecar_kzg_proofs(column_sidecar) - for column_sidecar in column_sidecars + DataColumnIdentifier(block_root=beacon_block_root, index=column_index) in store.data_column_sidecars + for column_index in custody_columns + ) +``` + +## New fork-choice handlers + +### New `on_data_column_sidecar` + +*Note*: `on_data_column_sidecar` is triggered whenever a data column sidecar is received (either through Gossipsub topics or Req/Resp). + +```python +def on_data_column_sidecar(store: Store, sidecar: DataColumnSidecar) -> None: + """ + Run ``on_data_column_sidecar`` upon receiving a data column sidecar. + """ + block_header = sidecar.signed_block_header.message + # Verify data column sidecar + assert verify_data_column_sidecar(sidecar) + # Data column sidecars cannot be in the future. If they are, their consideration must be delayed until they are in the past. + assert get_current_slot(store) >= block_header.slot + + # Check that the sidecar 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_header.slot > finalized_slot + + # Parent block must be known + assert block_header.parent_root in store.block_states + # Make a copy of the state to avoid mutability issues + state = copy(store.block_states[block_header.parent_root]) + + # The block header signature is valid with respect to the proposer pubkey + proposer = state.validators[block_header.proposer_index] + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block_header.slot)) + signing_root = compute_signing_root(block_header, domain) + assert bls.Verify(proposer.pubkey, signing_root, sidecar.signed_block_header.signature) + + # The sidecar is from a higher slot than the sidecar's block's parent + assert block_header.slot > store.blocks[block_header.parent_root].slot + + # Check block is a descendant of the finalized block at the checkpoint finalized slot + finalized_checkpoint_block = get_checkpoint_block( + store, + block_header.parent_root, + store.finalized_checkpoint.epoch, ) + assert store.finalized_checkpoint.root == finalized_checkpoint_block + + # The sidecar's inclusion proof is valid + assert verify_data_column_sidecar_inclusion_proof(sidecar) + + # The sidecar's column data is valid + assert verify_data_column_sidecar_kzg_proofs(sidecar) + + # Check block is proposed by the expected proposer + process_slots(state, block.slot) + assert block.proposer_index == get_beacon_proposer_index(state) + + # Save the data column sidecar + data_column_identifier = DataColumnIdentifier(block_root=hash_tree_root(block_header), index=sidecar.index) + store.data_column_sidecars[data_column_identifier] = sidecar ``` ## Updated fork-choice handlers diff --git a/specs/deneb/fork-choice.md b/specs/deneb/fork-choice.md index 836fe61743..8853fb46ac 100644 --- a/specs/deneb/fork-choice.md +++ b/specs/deneb/fork-choice.md @@ -24,6 +24,30 @@ This is the modification of the fork choice accompanying the Deneb upgrade. ## Helpers +### Modified `Store` + +*Note*: Blob sidecar store is added to keep track the blob sidecars that have been seen. + +```python +@dataclass +class Store(object): + time: uint64 + genesis_time: uint64 + justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + unrealized_justified_checkpoint: Checkpoint + unrealized_finalized_checkpoint: Checkpoint + proposer_boost_root: Root + equivocating_indices: Set[ValidatorIndex] + blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) + block_states: Dict[Root, BeaconState] = field(default_factory=dict) + block_timeliness: Dict[Root, boolean] = field(default_factory=dict) + checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) + latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) + unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) + blob_sidecars: Dict[BlobIdentifier, BlobSidecar] = field(default_factory=dict) # [New in Deneb] +``` + ### Extended `PayloadAttributes` `PayloadAttributes` is extended with the parent beacon block root for EIP-4788. @@ -43,21 +67,74 @@ class PayloadAttributes(object): *[New in Deneb:EIP4844]* The implementation of `is_data_available` will become more sophisticated during later scaling upgrades. -Initially, verification requires every verifying actor to retrieve all matching `Blob`s and `KZGProof`s, and validate them with `verify_blob_kzg_proof_batch`. The block MUST NOT be considered valid until all valid `Blob`s have been downloaded. Blocks that have been previously validated as available SHOULD be considered available even if the associated `Blob`s have subsequently been pruned. *Note*: Extraneous or invalid Blobs (in addition to KZG expected/referenced valid blobs) received on the p2p network MUST NOT invalidate a block that is otherwise valid and available. ```python -def is_data_available(beacon_block_root: Root, blob_kzg_commitments: Sequence[KZGCommitment]) -> bool: - # `retrieve_blobs_and_proofs` is implementation and context dependent - # It returns all the blobs for the given block root, and raises an exception if not available +def is_data_available(store: Store, beacon_block_root: Root, blob_kzg_commitments: Sequence[KZGCommitment]) -> bool: # Note: the p2p network does not guarantee sidecar retrieval outside of # `MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS` - blobs, proofs = retrieve_blobs_and_proofs(beacon_block_root) + return all( + BlobIdentifier(block_root=beacon_block_root, index=index) in store.blob_sidecars + for index in range(len(blob_kzg_commitments)) + ) +``` +## New fork-choice handlers + +### `on_blob_sidecar` + +```python +def on_blob_sidecar(store: Store, sidecar: BlobSidecar) -> None: + """ + Run ``on_blob_sidecar`` upon receiving a blob sidecar. + """ + block_header = sidecar.signed_block_header.message + # The sidecar's index is consistent with `MAX_BLOBS_PER_BLOCK` + assert sidecar.index < MAX_BLOBS_PER_BLOCK + # Blob sidecars cannot be in the future. If they are, their consideration must be delayed until they are in the past. + assert get_current_slot(store) >= block_header.slot + + # Check that the sidecar 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_header.slot > finalized_slot + + # Parent block must be known + assert block_header.parent_root in store.block_states + # Make a copy of the state to avoid mutability issues + state = copy(store.block_states[block_header.parent_root]) + + # The block header signature is valid with respect to the proposer pubkey + proposer = state.validators[block_header.proposer_index] + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block_header.slot)) + signing_root = compute_signing_root(block_header, domain) + assert bls.Verify(proposer.pubkey, signing_root, sidecar.signed_block_header.signature) + + # The sidecar is from a higher slot than the sidecar's block's parent + assert block_header.slot > store.blocks[block_header.parent_root].slot + + # Check block is a descendant of the finalized block at the checkpoint finalized slot + finalized_checkpoint_block = get_checkpoint_block( + store, + block_header.parent_root, + store.finalized_checkpoint.epoch, + ) + assert store.finalized_checkpoint.root == finalized_checkpoint_block + + # The sidecar's inclusion proof is valid + assert verify_blob_sidecar_inclusion_proof(sidecar) + + # The sidecar's blob is valid + asesrt verify_blob_kzg_proof(sidecar.blob, sidecar.kzg_commitment, sidecar.kzg_proof) + + # Check block is proposed by the expected proposer + process_slots(state, block.slot) + assert block.proposer_index == get_beacon_proposer_index(state) - return verify_blob_kzg_proof_batch(blobs, blob_kzg_commitments, proofs) + # Save the blob sidecar + blob_identifier = BlobIdentifier(block_root=hash_tree_root(block_header), index=sidecar.index) + store.blob_sidecars[blob_identifier] = sidecar ``` ## Updated fork-choice handlers