diff --git a/pysetup/spec_builders/gloas.py b/pysetup/spec_builders/gloas.py index 307fe0ecac..5f42c8a3dd 100644 --- a/pysetup/spec_builders/gloas.py +++ b/pysetup/spec_builders/gloas.py @@ -45,9 +45,6 @@ def deprecate_functions(cls) -> set[str]: "retrieve_column_sidecars", "upgrade_to_fulu", "verify_partial_data_column_header_inclusion_proof", - # TODO(jtraglia): Temporarily deprecate these until we update them for Gloas. - "validate_data_column_sidecar_gossip", - "validate_partial_data_column_sidecar_gossip", } @classmethod diff --git a/specs/fulu/p2p-interface.md b/specs/fulu/p2p-interface.md index cb1ebea4f1..937c2c064d 100644 --- a/specs/fulu/p2p-interface.md +++ b/specs/fulu/p2p-interface.md @@ -249,14 +249,12 @@ def validate_beacon_block_gossip( state: BeaconState, signed_beacon_block: SignedBeaconBlock, current_time_ms: uint64, - block_payload_statuses: Optional[Dict[Root, PayloadValidationStatus]] = None, + block_payload_statuses: Dict[Root, PayloadValidationStatus], ) -> None: """ Validate a SignedBeaconBlock for gossip propagation. Raises GossipIgnore or GossipReject on validation failure. """ - if block_payload_statuses is None: - block_payload_statuses = {} block = signed_beacon_block.message execution_payload = block.body.execution_payload diff --git a/specs/fulu/partial-columns/p2p-interface.md b/specs/fulu/partial-columns/p2p-interface.md index 7132207660..78725704a1 100644 --- a/specs/fulu/partial-columns/p2p-interface.md +++ b/specs/fulu/partial-columns/p2p-interface.md @@ -4,14 +4,15 @@ - [Introduction](#introduction) - [Containers](#containers) - - [`PartialDataColumnSidecar`](#partialdatacolumnsidecar) - - [`PartialDataColumnPartsMetadata`](#partialdatacolumnpartsmetadata) - - [`PartialDataColumnHeader`](#partialdatacolumnheader) + - [New `PartialDataColumnSidecar`](#new-partialdatacolumnsidecar) + - [New `PartialDataColumnPartsMetadata`](#new-partialdatacolumnpartsmetadata) + - [New `PartialDataColumnHeader`](#new-partialdatacolumnheader) + - [New `PartialDataColumnGroupID`](#new-partialdatacolumngroupid) - [Helpers](#helpers) - - [`verify_partial_data_column_header_inclusion_proof`](#verify_partial_data_column_header_inclusion_proof) - - [`verify_partial_data_column_sidecar_kzg_proofs`](#verify_partial_data_column_sidecar_kzg_proofs) + - [New `verify_partial_data_column_header_inclusion_proof`](#new-verify_partial_data_column_header_inclusion_proof) + - [New `verify_partial_data_column_sidecar_kzg_proofs`](#new-verify_partial_data_column_sidecar_kzg_proofs) - [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) - - [Partial Messages on `data_column_sidecar_{subnet_id}`](#partial-messages-on-data_column_sidecar_subnet_id) + - [New `data_column_sidecar_{subnet_id}` (partial messages)](#new-data_column_sidecar_subnet_id-partial-messages) - [Partial columns for Cell Dissemination](#partial-columns-for-cell-dissemination) - [Partial message group ID](#partial-message-group-id) - [Parts metadata](#parts-metadata) @@ -40,7 +41,7 @@ particular, this document builds on the ## Containers -### `PartialDataColumnSidecar` +### New `PartialDataColumnSidecar` The `PartialDataColumnSidecar` is similar to the `DataColumnSidecar` container, except that only the cells and proofs identified by the bitmap are present. @@ -56,7 +57,7 @@ class PartialDataColumnSidecar(Container): header: List[PartialDataColumnHeader, 1] ``` -### `PartialDataColumnPartsMetadata` +### New `PartialDataColumnPartsMetadata` Peers communicate the cells available with a bitmap. A set bit (`1`) at index `i` means that the peer has the cell at index `i`. Peers explicitly request @@ -86,7 +87,7 @@ bit from the requests bit. Having a cell but not willing to provide it is functionally the same as not having the cell and not wanting it, so it does not need a separate state. -### `PartialDataColumnHeader` +### New `PartialDataColumnHeader` The `PartialDataColumnHeader` is the header that is common to all columns for a given block. It lets a peer identify which blobs are included in a block, as @@ -101,9 +102,16 @@ class PartialDataColumnHeader(Container): kzg_commitments_inclusion_proof: Vector[Bytes32, KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH] ``` +### New `PartialDataColumnGroupID` + +```python +class PartialDataColumnGroupID(Container): + block_root: Root +``` + ## Helpers -### `verify_partial_data_column_header_inclusion_proof` +### New `verify_partial_data_column_header_inclusion_proof` ```python def verify_partial_data_column_header_inclusion_proof(header: PartialDataColumnHeader) -> bool: @@ -119,7 +127,7 @@ def verify_partial_data_column_header_inclusion_proof(header: PartialDataColumnH ) ``` -### `verify_partial_data_column_sidecar_kzg_proofs` +### New `verify_partial_data_column_sidecar_kzg_proofs` ```python def verify_partial_data_column_sidecar_kzg_proofs( @@ -147,7 +155,7 @@ def verify_partial_data_column_sidecar_kzg_proofs( ## The gossip domain: gossipsub -### Partial Messages on `data_column_sidecar_{subnet_id}` +### New `data_column_sidecar_{subnet_id}` (partial messages) *Note*: Validating partial messages happens in two parts. First, the `PartialDataColumnHeader` needs to be validated, then the cell and proof data. @@ -156,15 +164,24 @@ subnet (gossipsub topic), it can be used for all subnets. Due to the nature of partial messages, it is possible to get the `PartialDataColumnHeader` with no cells, and get cells in a future response. +*Note*: The Partial Message Group ID is the SSZ encoded +`PartialDataColumnGroupID` prefixed with the version byte `0x00`. +Implementations MUST ignore unknown versions. + +*Note*: The optional check "for cells the receiver already has, the sidecar's +cell and proof data are equal to the local copy" is not encoded above. The +sender MUST always send valid cell and proof data; receivers MAY perform this +equality check against their local copy as an additional safeguard. + ```python def validate_partial_data_column_sidecar_gossip( seen: Seen, store: Store, state: BeaconState, sidecar: PartialDataColumnSidecar, - block_root: Root, - column_index: ColumnIndex, current_time_ms: uint64, + group_id: PartialDataColumnGroupID, + column_index: ColumnIndex, ) -> None: """ Validate a PartialDataColumnSidecar for gossip propagation on a subnet. @@ -178,11 +195,11 @@ def validate_partial_data_column_sidecar_gossip( if not (has_header or has_cells): raise GossipReject("partial message is semantically empty") - # [REJECT] The cell count equals the number of bits set in cells_present_bitmap + # [REJECT] The cell count equals the number of set bits in the bitmap if len(sidecar.partial_column) != num_cells_present: raise GossipReject("number of cells does not match number of set bits") - # [REJECT] The proof count equals the number of bits set in cells_present_bitmap + # [REJECT] The proof count equals the number of set bits in the bitmap if len(sidecar.kzg_proofs) != num_cells_present: raise GossipReject("number of proofs does not match number of set bits") @@ -191,13 +208,13 @@ def validate_partial_data_column_sidecar_gossip( block_header = header.signed_block_header.message # [REJECT] The received header MUST equal any previously validated header for this block - prior_header = seen.partial_data_column_headers.get(block_root) + prior_header = seen.partial_data_column_headers.get(group_id.block_root) if prior_header is not None and prior_header != header: raise GossipReject("header differs from previously validated header") # [REJECT] The signed_block_header hash matches the partial message's group id - if hash_tree_root(block_header) != block_root: - raise GossipReject("header's block root does not match partial message group id") + if hash_tree_root(block_header) != group_id.block_root: + raise GossipReject("header's block root does not match group id's block root") # [REJECT] The header's kzg_commitments list is non-empty if len(header.kzg_commitments) == 0: @@ -257,11 +274,11 @@ def validate_partial_data_column_sidecar_gossip( raise GossipReject("header proposer_index does not match expected proposer") # Mark this header as seen - seen.partial_data_column_headers[block_root] = header + seen.partial_data_column_headers[group_id.block_root] = header if has_cells: # [IGNORE] A valid corresponding PartialDataColumnHeader has been seen - header = seen.partial_data_column_headers.get(block_root) + header = seen.partial_data_column_headers.get(group_id.block_root) if header is None: raise GossipIgnore("valid corresponding header has not been seen") @@ -279,22 +296,17 @@ def validate_partial_data_column_sidecar_gossip( "corresponding header is not from a slot greater than the latest finalized slot" ) - # [REJECT] The cells_present_bitmap length equals the number of header kzg_commitments + # [REJECT] The cells present bitmap length equals the number of bid commitments if len(sidecar.cells_present_bitmap) != len(header.kzg_commitments): raise GossipReject("bitmap length does not match commitments length") - # [REJECT] The sidecar's cell and proof data is valid + # [REJECT] The sidecar's cell and proof data passes KZG verification if not verify_partial_data_column_sidecar_kzg_proofs( sidecar, header.kzg_commitments, column_index ): raise GossipReject("invalid sidecar kzg proofs") ``` -*Note*: The optional check "for cells the receiver already has, the sidecar's -cell and proof data are equal to the local copy" is not encoded above. The -sender MUST always send valid cell and proof data; receivers MAY perform this -equality check against their local copy as an additional safeguard. - ### Partial columns for Cell Dissemination Gossipsub's @@ -305,9 +317,10 @@ cells along with their proofs. #### Partial message group ID -When sending a partial message, the gossipsub group ID MUST be the block root -prefixed by a single byte used for versioning. The version byte MUST be zero. -Other versions may be defined later. +When sending a partial message, the gossipsub group ID MUST be the SSZ encoded +`PartialDataColumnGroupID` prefixed with a single version byte. The version byte +MUST be `0x00`. Implementations MUST ignore unknown versions. Other versions may +be defined later. #### Parts metadata diff --git a/specs/gloas/p2p-interface.md b/specs/gloas/p2p-interface.md index 1a21cfcd41..adedc0cee2 100644 --- a/specs/gloas/p2p-interface.md +++ b/specs/gloas/p2p-interface.md @@ -16,19 +16,23 @@ - [Modified `compute_fork_version`](#modified-compute_fork_version) - [Modified `verify_data_column_sidecar_kzg_proofs`](#modified-verify_data_column_sidecar_kzg_proofs) - [Modified `verify_data_column_sidecar`](#modified-verify_data_column_sidecar) + - [New `is_current_or_next_slot`](#new-is_current_or_next_slot) + - [New `is_gas_limit_target_compatible`](#new-is_gas_limit_target_compatible) + - [New `is_valid_proposal_slot`](#new-is_valid_proposal_slot) + - [New `get_proposer_dependent_root`](#new-get_proposer_dependent_root) - [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) - [Topics and messages](#topics-and-messages) - [Global topics](#global-topics) - - [`beacon_aggregate_and_proof`](#beacon_aggregate_and_proof) - - [`beacon_block`](#beacon_block) - - [`execution_payload`](#execution_payload) - - [`payload_attestation_message`](#payload_attestation_message) - - [`execution_payload_bid`](#execution_payload_bid) - - [`proposer_preferences`](#proposer_preferences) + - [Modified `beacon_aggregate_and_proof`](#modified-beacon_aggregate_and_proof) + - [Modified `beacon_block`](#modified-beacon_block) + - [New `execution_payload`](#new-execution_payload) + - [New `payload_attestation_message`](#new-payload_attestation_message) + - [New `execution_payload_bid`](#new-execution_payload_bid) + - [New `proposer_preferences`](#new-proposer_preferences) - [Blob subnets](#blob-subnets) - - [`data_column_sidecar_{subnet_id}`](#data_column_sidecar_subnet_id) + - [Modified `data_column_sidecar_{subnet_id}`](#modified-data_column_sidecar_subnet_id) - [Attestation subnets](#attestation-subnets) - - [`beacon_attestation_{subnet_id}`](#beacon_attestation_subnet_id) + - [Modified `beacon_attestation_{subnet_id}`](#modified-beacon_attestation_subnet_id) - [The Req/Resp domain](#the-reqresp-domain) - [Messages](#messages) - [BeaconBlocksByRange v2](#beaconblocksbyrange-v2) @@ -122,9 +126,22 @@ class Seen: sync_contribution_data: Dict[Tuple[Slot, Root, uint64], Set[Tuple[boolean, ...]]] sync_message_validator_slots: Set[Tuple[Slot, ValidatorIndex, uint64]] bls_to_execution_change_indices: Set[ValidatorIndex] - data_column_sidecar_tuples: Set[Tuple[Slot, ValidatorIndex, ColumnIndex]] + # [Modified in Gloas:EIP7732] + data_column_sidecar_tuples: Set[Tuple[Root, ColumnIndex]] # [Modified in Gloas:EIP7732] # Removed `partial_data_column_headers` + # [New in Gloas:EIP7732] + execution_payloads: Dict[Hash32, ExecutionPayload] + # [New in Gloas:EIP7732] + execution_payload_envelopes: Set[Tuple[Root, BuilderIndex]] + # [New in Gloas:EIP7732] + payload_attestation_validators: Set[Tuple[Slot, ValidatorIndex]] + # [New in Gloas:EIP7732] + execution_payload_bids: Set[Tuple[BuilderIndex, Slot]] + # [New in Gloas:EIP7732] + best_execution_payload_bid: Dict[Tuple[Slot, Hash32, Root], Gwei] + # [New in Gloas:EIP7732] + proposer_preferences: Dict[Tuple[Root, Slot], ProposerPreferences] ``` #### Modified `compute_fork_version` @@ -205,6 +222,74 @@ def verify_data_column_sidecar( return True ``` +#### New `is_current_or_next_slot` + +```python +def is_current_or_next_slot( + state: BeaconState, + slot: Slot, + current_time_ms: uint64, +) -> bool: + """ + Check if ``slot`` is the current slot or the next slot + (with MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). + """ + return is_within_slot_range(state, slot, 1, current_time_ms + SLOT_DURATION_MS) +``` + +#### New `is_gas_limit_target_compatible` + +```python +def is_gas_limit_target_compatible( + parent_gas_limit: uint64, gas_limit: uint64, target_gas_limit: uint64 +) -> bool: + """ + Check if ``gas_limit`` is compatible with ``target_gas_limit`` under the + EIP-1559 transition rule from ``parent_gas_limit``. + """ + max_gas_limit_difference = max(parent_gas_limit // 1024, 1) - 1 + min_gas_limit = parent_gas_limit - max_gas_limit_difference + max_gas_limit = parent_gas_limit + max_gas_limit_difference + + if target_gas_limit >= min_gas_limit and target_gas_limit <= max_gas_limit: + return gas_limit == target_gas_limit + if target_gas_limit > max_gas_limit: + return gas_limit == max_gas_limit + return gas_limit == min_gas_limit +``` + +#### New `is_valid_proposal_slot` + +```python +def is_valid_proposal_slot(state: BeaconState, preferences: ProposerPreferences) -> bool: + """ + Check if the validator is the proposer for the given slot within the + proposer lookahead. + """ + current_epoch = get_current_epoch(state) + proposal_epoch = compute_epoch_at_slot(preferences.proposal_slot) + if proposal_epoch < current_epoch: + return False + if proposal_epoch > current_epoch + Epoch(MIN_SEED_LOOKAHEAD): + return False + + index = (proposal_epoch - current_epoch) * SLOTS_PER_EPOCH + index += preferences.proposal_slot % SLOTS_PER_EPOCH + return state.proposer_lookahead[index] == preferences.validator_index +``` + +#### New `get_proposer_dependent_root` + +```python +def get_proposer_dependent_root(state: BeaconState, epoch: Epoch) -> Root: + """ + Return the dependent root for the proposer lookahead at ``epoch``. + """ + return get_block_root_at_slot( + state, Slot(compute_start_slot_at_epoch(Epoch(epoch - MIN_SEED_LOOKAHEAD)) - 1) + ) +``` + ### The gossip domain: gossipsub Some gossip meshes are upgraded in Gloas to support upgraded types. @@ -231,185 +316,510 @@ are given in this table: ##### Global topics -Gloas introduces new global topics for execution bid, execution payload and -payload attestation. - -###### `beacon_aggregate_and_proof` - -Let `block` be the beacon block corresponding to -`aggregate.data.beacon_block_root`. +###### Modified `beacon_aggregate_and_proof` -The following validations are added: +*Note*: This function is modified per EIP-7732. `aggregate.data.index` is now +restricted to `{0, 1}`, encoding whether the execution payload was present at +the slot. Same-slot aggregates MUST attest with `index == 0`. Aggregates with +`index == 1` require that the corresponding execution payload envelope has been +seen and passes execution-layer validation. -- _[REJECT]_ `aggregate.data.index < 2`. -- _[REJECT]_ `aggregate.data.index == 0` if `block.slot == aggregate.data.slot`. -- _[REJECT]_ If `aggregate.data.index == 1` (payload present for a past block) - the corresponding execution payload for `block` passes validation. -- _[IGNORE]_ When `aggregate.data.index == 1` (payload present for a past - block), the corresponding execution payload for `block` has been seen (a - client MAY queue attestations for processing once the payload is retrieved and - SHOULD request the payload envelope via `ExecutionPayloadEnvelopesByRoot` - using `aggregate.data.beacon_block_root`). - -The following validations are removed: +```python +def validate_beacon_aggregate_and_proof_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_aggregate_and_proof: SignedAggregateAndProof, + current_time_ms: uint64, + # [New in Gloas:EIP7732] + block_payload_statuses: Dict[Root, PayloadValidationStatus], +) -> None: + """ + Validate a SignedAggregateAndProof for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + aggregate_and_proof = signed_aggregate_and_proof.message + aggregate = aggregate_and_proof.aggregate + aggregation_bits = aggregate.aggregation_bits -- _[REJECT]_ `aggregate.data.index == 0`. + # [New in Gloas:EIP7732] + # [REJECT] The aggregate attestation's data index is 0 or 1 + if aggregate.data.index > 1: + raise GossipReject("aggregate data index must be 0 or 1") + + # [REJECT] Exactly one committee is specified by the committee bits + committee_indices = get_committee_indices(aggregate.committee_bits) + if len(committee_indices) != 1: + raise GossipReject("aggregate committee bits must specify exactly one committee") + index = committee_indices[0] + + # [REJECT] The committee index is within the expected range + committee_count = get_committee_count_per_slot(state, aggregate.data.target.epoch) + if index >= committee_count: + raise GossipReject("committee index out of range") + + # [IGNORE] The aggregate attestation's slot is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, aggregate.data.slot, current_time_ms): + raise GossipIgnore("aggregate slot is from a future slot") + + # [IGNORE] The aggregate attestation's epoch is either the current or previous epoch + attestation_epoch = compute_epoch_at_slot(aggregate.data.slot) + is_previous_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(Epoch(attestation_epoch + 1)), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + is_current_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(attestation_epoch), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + if not (is_previous_epoch_attestation or is_current_epoch_attestation): + raise GossipIgnore("aggregate epoch is not previous or current epoch") + + # [REJECT] The aggregate attestation's epoch matches its target + if aggregate.data.target.epoch != compute_epoch_at_slot(aggregate.data.slot): + raise GossipReject("attestation epoch does not match target epoch") + + # [REJECT] The number of aggregation bits matches the committee size + committee = get_beacon_committee(state, aggregate.data.slot, index) + if len(aggregation_bits) != len(committee): + raise GossipReject("aggregation bits length does not match committee size") + + # [REJECT] The aggregate attestation has participants + attesting_indices = get_attesting_indices(state, aggregate) + if len(attesting_indices) < 1: + raise GossipReject("aggregate has no participants") + + # [IGNORE] A valid aggregate with a superset of aggregation bits has not already been seen + aggregate_data_root = hash_tree_root(aggregate.data) + aggregate_cache_key = (aggregate_data_root, index) + aggregate_bits = tuple(bool(bit) for bit in aggregation_bits) + seen_bits = seen.aggregate_data_roots.get(aggregate_cache_key, set()) + if is_non_strict_superset(seen_bits, aggregate_bits): + raise GossipIgnore("already seen aggregate for this data") + + # [IGNORE] This is the first valid aggregate for this aggregator in this epoch + aggregator_index = aggregate_and_proof.aggregator_index + target_epoch = aggregate.data.target.epoch + if (aggregator_index, target_epoch) in seen.aggregator_epochs: + raise GossipIgnore("already seen aggregate from this aggregator for this epoch") + + # [REJECT] The selection proof selects the validator as an aggregator + if not is_aggregator(state, aggregate.data.slot, index, aggregate_and_proof.selection_proof): + raise GossipReject("validator is not selected as aggregator") + + # [REJECT] The aggregator's validator index is within the committee + if aggregator_index not in committee: + raise GossipReject("aggregator index not in committee") + + # [REJECT] The selection proof signature is valid + aggregator = state.validators[aggregator_index] + domain = get_domain(state, DOMAIN_SELECTION_PROOF, target_epoch) + signing_root = compute_signing_root(aggregate.data.slot, domain) + if not bls.Verify(aggregator.pubkey, signing_root, aggregate_and_proof.selection_proof): + raise GossipReject("invalid selection proof signature") + + # [REJECT] The aggregator signature is valid + domain = get_domain(state, DOMAIN_AGGREGATE_AND_PROOF, target_epoch) + signing_root = compute_signing_root(aggregate_and_proof, domain) + if not bls.Verify(aggregator.pubkey, signing_root, signed_aggregate_and_proof.signature): + raise GossipReject("invalid aggregator signature") + + # [REJECT] The aggregate signature is valid + if not is_valid_indexed_attestation(state, get_indexed_attestation(state, aggregate)): + raise GossipReject("invalid aggregate signature") + + # [IGNORE] The block being voted for has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + block_root = aggregate.data.beacon_block_root + if block_root not in store.blocks: + raise GossipIgnore("block being voted for has not been seen") + + # [REJECT] The block being voted for passes validation + if block_root not in store.block_states: + raise GossipReject("block being voted for failed validation") + + block = store.blocks[block_root] -###### `beacon_block` + # [New in Gloas:EIP7732] + # [REJECT] For same-slot aggregates, the payload cannot yet be present + if block.slot == aggregate.data.slot and aggregate.data.index != 0: + raise GossipReject("same-slot aggregate must attest with index 0") + + if aggregate.data.index == 1: + # [New in Gloas:EIP7732] + # [IGNORE] The corresponding execution payload envelope has been seen + # (MAY queue attestations for processing once the payload is retrieved and + # SHOULD request the payload envelope via ExecutionPayloadEnvelopesByRoot + # using aggregate.data.beacon_block_root) + payload_status = block_payload_statuses.get(block_root) + if payload_status is None: + raise GossipIgnore("execution payload envelope has not been seen") + + # [New in Gloas:EIP7732] + # [IGNORE] The corresponding execution payload has been validated + if payload_status == PAYLOAD_STATUS_NOT_VALIDATED: + raise GossipIgnore("execution payload pending EL validation") + + # [New in Gloas:EIP7732] + # [REJECT] The corresponding execution payload passes EL validation + if payload_status == PAYLOAD_STATUS_INVALIDATED: + raise GossipReject("execution payload failed EL validation") + + # [REJECT] The target block is an ancestor of the LMD vote block + checkpoint_block = get_checkpoint_block(store, block_root, aggregate.data.target.epoch) + if checkpoint_block != aggregate.data.target.root: + raise GossipReject("target block is not an ancestor of LMD vote block") + + # [IGNORE] The finalized checkpoint is an ancestor of the block + finalized_checkpoint_block = get_checkpoint_block( + store, block_root, store.finalized_checkpoint.epoch + ) + if finalized_checkpoint_block != store.finalized_checkpoint.root: + raise GossipIgnore("finalized checkpoint is not an ancestor of block") + + # Mark this aggregate as seen + seen.aggregator_epochs.add((aggregator_index, target_epoch)) + if aggregate_cache_key not in seen.aggregate_data_roots: + seen.aggregate_data_roots[aggregate_cache_key] = set() + seen.aggregate_data_roots[aggregate_cache_key].add(aggregate_bits) +``` -*[Modified in Gloas:EIP7732]* +###### Modified `beacon_block` -The *type* of the payload of this topic changes to the (modified) -`SignedBeaconBlock` found in [the beacon-chain changes](./beacon-chain.md). +*Note*: This function is modified per EIP-7732. The execution payload is no +longer carried inside `BeaconBlock`. As a result, all validations referring to +`block.body.execution_payload` are removed and replaced with validations of the +bid carried at `block.body.signed_execution_payload_bid.message`. -There are no new validations for this topic. However, all validations with -regards to the `ExecutionPayload` are removed: +```python +def validate_beacon_block_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_beacon_block: SignedBeaconBlock, + current_time_ms: uint64, + # [Modified in Gloas:EIP7732] + # Removed `block_payload_statuses` +) -> None: + """ + Validate a SignedBeaconBlock for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + block = signed_beacon_block.message + # [New in Gloas:EIP7732] + bid = block.body.signed_execution_payload_bid.message + + # [IGNORE] The block is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, block.slot, current_time_ms): + raise GossipIgnore("block is from a future slot") + + # [IGNORE] The block is from a slot greater than the latest finalized slot + # (MAY choose to validate and store such blocks for additional purposes + # -- e.g. slashing detection, archive nodes, etc) + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + if block.slot <= finalized_slot: + raise GossipIgnore("block is not from a slot greater than the latest finalized slot") + + # [IGNORE] The block is the first block with valid signature received for the proposer for the slot + if (block.proposer_index, block.slot) in seen.proposer_slots: + raise GossipIgnore("block is not the first valid block for this proposer and slot") + + # [REJECT] The proposer index is a valid validator index + if block.proposer_index >= len(state.validators): + raise GossipReject("proposer index out of range") + + # [REJECT] The proposer signature is valid + proposer = state.validators[block.proposer_index] + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block.slot)) + signing_root = compute_signing_root(block, domain) + if not bls.Verify(proposer.pubkey, signing_root, signed_beacon_block.signature): + raise GossipReject("invalid proposer signature") + + # [IGNORE] The block's parent has been seen (via gossip or non-gossip sources) + # (MAY be queued until parent is retrieved) + if block.parent_root not in store.blocks: + raise GossipIgnore("block's parent has not been seen") + + # [REJECT] The block is from a higher slot than its parent + if block.slot <= store.blocks[block.parent_root].slot: + raise GossipReject("block is not from a higher slot than its parent") + + # [REJECT] The current finalized checkpoint is an ancestor of the block + checkpoint_block = get_checkpoint_block( + store, block.parent_root, store.finalized_checkpoint.epoch + ) + if checkpoint_block != store.finalized_checkpoint.root: + raise GossipReject("finalized checkpoint is not an ancestor of block") -- _[REJECT]_ The block's execution payload timestamp is correct with respect to - the slot -- i.e. - `execution_payload.timestamp == compute_time_at_slot(state, block.slot)`. -- If `execution_payload` verification of block's parent by an execution node is - *not* complete: - - [REJECT] The block's parent (defined by `block.parent_root`) passes all - validation (excluding execution node verification of the - `block.body.execution_payload`). -- otherwise: - - [IGNORE] The block's parent (defined by `block.parent_root`) passes all - validation (including execution node verification of the - `block.body.execution_payload`). + # [Modified in Gloas:EIP7732] + # [REJECT] The bid's blob KZG commitment count is within the per-epoch limit + max_blobs = get_blob_parameters(get_current_epoch(state)).max_blobs_per_block + if len(bid.blob_kzg_commitments) > max_blobs: + raise GossipReject("too many blob kzg commitments") -And instead the following validations are set in place with the alias -`bid = block.body.signed_execution_payload_bid.message`: + # [Modified in Gloas:EIP7732] + # [REJECT] The bid's parent equals the block's parent + if bid.parent_block_root != block.parent_root: + raise GossipReject("bid's parent does not equal block's parent") -- _[REJECT]_ The length of KZG commitments is less than or equal to the - limitation defined in the consensus layer -- i.e. validate that - `len(bid.blob_kzg_commitments) <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block` -- _[IGNORE]_ The block's parent execution payload (defined by - `bid.parent_block_hash`) has been seen (via gossip or non-gossip sources) (a - client MAY queue blocks for processing once the parent payload is retrieved). -- If `execution_payload` verification of block's execution payload parent by an - execution node **is complete**: - - [REJECT] The block's execution payload parent (defined by - `bid.parent_block_hash`) passes all validation. -- [REJECT] The bid's parent (defined by `bid.parent_block_root`) equals the - block's parent (defined by `block.parent_root`). + # [New in Gloas:EIP7732] + # [IGNORE] The block's parent state is available + # (MAY be queued until state transition is complete) + if block.parent_root not in store.block_states: + raise GossipIgnore("block's parent state is unavailable") + + # [REJECT] The block is proposed by the expected proposer for the slot + parent_state = store.block_states[block.parent_root].copy() + process_slots(parent_state, block.slot) + expected_proposer = get_beacon_proposer_index(parent_state) + if block.proposer_index != expected_proposer: + raise GossipReject("block proposer does not match the expected proposer") + + # Mark this block as seen + seen.proposer_slots.add((block.proposer_index, block.slot)) +``` -###### `execution_payload` +###### New `execution_payload` This topic is used to propagate execution payload messages as `SignedExecutionPayloadEnvelope`. -The following validations MUST pass before forwarding the -`signed_execution_payload_envelope` on the network, assuming the alias -`envelope = signed_execution_payload_envelope.message`, -`payload = envelope.payload`: - -- _[IGNORE]_ The envelope's block root `envelope.beacon_block_root` has been - seen (via gossip or non-gossip sources) (a client MAY queue payload for - processing once the block is retrieved). -- _[IGNORE]_ The node has not seen another valid - `SignedExecutionPayloadEnvelope` for this block root from this builder. -- _[IGNORE]_ The envelope is from a slot greater than or equal to the latest - finalized slot -- i.e. validate that - `envelope.payload.slot_number >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)` - -Let `block` be the block with `envelope.beacon_block_root`. Let `bid` alias -`block.body.signed_execution_payload_bid.message` (notice that this can be -obtained from the `state.latest_execution_payload_bid`) - -- _[REJECT]_ `block` passes validation. -- _[REJECT]_ `block.slot` equals `envelope.payload.slot_number`. -- _[REJECT]_ `envelope.builder_index == bid.builder_index` -- _[REJECT]_ `payload.block_hash == bid.block_hash` -- _[REJECT]_ - `hash_tree_root(envelope.execution_requests) == bid.execution_requests_root` -- _[REJECT]_ `signed_execution_payload_envelope.signature` is valid as verified - by `verify_execution_payload_envelope_signature`. - -###### `payload_attestation_message` +```python +def validate_execution_payload_envelope_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_execution_payload_envelope: SignedExecutionPayloadEnvelope, +) -> None: + """ + Validate a SignedExecutionPayloadEnvelope for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + envelope = signed_execution_payload_envelope.message + payload = envelope.payload + block_root = envelope.beacon_block_root + + # [IGNORE] The envelope's block root has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + if block_root not in store.blocks: + raise GossipIgnore("envelope's block has not been seen") + + # [IGNORE] The node has not seen another valid envelope for this block root from this builder + envelope_key = (block_root, envelope.builder_index) + if envelope_key in seen.execution_payload_envelopes: + raise GossipIgnore("already seen envelope for this block root from this builder") + + # [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + if payload.slot_number < finalized_slot: + raise GossipIgnore("envelope is from a slot before the latest finalized slot") + + # [REJECT] The envelope's block passes validation + if block_root not in store.block_states: + raise GossipReject("envelope's block failed validation") + + block = store.blocks[block_root] + bid = block.body.signed_execution_payload_bid.message + + # [REJECT] The block's slot matches the payload's slot number + if block.slot != payload.slot_number: + raise GossipReject("block's slot does not match payload's slot number") + + # [REJECT] The envelope is from the builder committed to by the bid + if envelope.builder_index != bid.builder_index: + raise GossipReject("envelope's builder index does not match the bid's builder index") + + # [REJECT] The payload's block hash matches the bid's block hash + if payload.block_hash != bid.block_hash: + raise GossipReject("payload's block hash does not match the bid's block hash") + + # [REJECT] The envelope's execution requests root matches the bid's execution requests root + if hash_tree_root(envelope.execution_requests) != bid.execution_requests_root: + raise GossipReject("envelope's execution requests root does not match the bid") + + # [REJECT] The envelope signature is valid + if not verify_execution_payload_envelope_signature(state, signed_execution_payload_envelope): + raise GossipReject("invalid envelope signature") + + # Mark this envelope as seen and store its payload + seen.execution_payload_envelopes.add(envelope_key) + seen.execution_payloads[payload.block_hash] = payload +``` + +###### New `payload_attestation_message` This topic is used to propagate signed payload attestation message. -The following validations MUST pass before forwarding the -`payload_attestation_message` on the network, assuming the alias -`data = payload_attestation_message.data`: - -- _[IGNORE]_ The message's slot is for the current slot (with a - `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance), i.e. `data.slot == current_slot`. -- _[IGNORE]_ The `payload_attestation_message` is the first valid message - received from the validator with index - `payload_attestation_message.validator_index`. -- _[IGNORE]_ The message's block `data.beacon_block_root` has been seen (via - gossip or non-gossip sources) (a client MAY queue attestation for processing - once the block is retrieved. Note a client might want to request payload - after). -- _[IGNORE]_ The block referenced by `data.beacon_block_root` is at slot - `data.slot`, i.e. the block has `block.slot == data.slot`. -- _[REJECT]_ The message's block `data.beacon_block_root` passes validation. -- _[REJECT]_ The message's validator index is within the payload committee in - `get_ptc(state, data.slot)`. The `state` is the head state corresponding to - processing the block up to the current slot as determined by the fork choice. -- _[REJECT]_ `payload_attestation_message.signature` is valid with respect to - the validator's public key. - -###### `execution_payload_bid` +```python +def validate_payload_attestation_message_gossip( + seen: Seen, + store: Store, + state: BeaconState, + payload_attestation_message: PayloadAttestationMessage, + current_time_ms: uint64, +) -> None: + """ + Validate a PayloadAttestationMessage for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + data = payload_attestation_message.data + validator_index = payload_attestation_message.validator_index + + # [IGNORE] The message's slot is the current slot + if not is_current_slot(state, data.slot, current_time_ms): + raise GossipIgnore("payload attestation message slot is not the current slot") + + # [IGNORE] The message is the first valid message from this validator index + seen_key = (data.slot, validator_index) + if seen_key in seen.payload_attestation_validators: + raise GossipIgnore("already seen payload attestation message from this validator") + + # [IGNORE] The message's block has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + if data.beacon_block_root not in store.blocks: + raise GossipIgnore("message's block has not been seen") + + # [IGNORE] The message's block is at the assigned slot + if store.blocks[data.beacon_block_root].slot != data.slot: + raise GossipIgnore("message's block is not at the assigned slot") + + # [REJECT] The message's block passes validation + if data.beacon_block_root not in store.block_states: + raise GossipReject("message's block failed validation") + + # [REJECT] The validator index is valid + if validator_index >= len(state.validators): + raise GossipReject("validator index out of range") + + # [REJECT] The validator is a member of the payload committee + if validator_index not in get_ptc(state, data.slot): + raise GossipReject("validator is not in the payload timeliness committee") + + # [REJECT] The signature is valid with respect to the validator's public key + validator = state.validators[validator_index] + domain = get_domain(state, DOMAIN_PTC_ATTESTER, compute_epoch_at_slot(data.slot)) + signing_root = compute_signing_root(data, domain) + if not bls.Verify(validator.pubkey, signing_root, payload_attestation_message.signature): + raise GossipReject("invalid payload attestation message signature") + + # Mark this message as seen + seen.payload_attestation_validators.add(seen_key) +``` + +###### New `execution_payload_bid` This topic is used to propagate signed bids as `SignedExecutionPayloadBid`. -The following validations MUST pass before forwarding the -`signed_execution_payload_bid` on the network, assuming the alias -`bid = signed_execution_payload_bid.message`, the alias -`signed_proposer_preferences` for the validated `SignedProposerPreferences` -whose `message.proposal_slot` is `bid.slot` and `message.dependent_root` is -`get_proposer_dependent_root(parent_state, compute_epoch_at_slot(bid.slot))`, -where `parent_state` is the post-state of `bid.parent_block_root`, and the alias -`proposer_preferences = signed_proposer_preferences.message`: - -- _[IGNORE]_ `bid.slot` is the current slot or the next slot. -- _[IGNORE]_ The matching `signed_proposer_preferences` has been seen. -- _[REJECT]_ `bid.builder_index` is a valid/active builder index -- i.e. - `is_active_builder(state, bid.builder_index)` returns `True`. -- _[REJECT]_ `bid.execution_payment == 0`. -- _[REJECT]_ `bid.fee_recipient == proposer_preferences.fee_recipient`. -- _[REJECT]_ The length of KZG commitments is less than or equal to the - limitation defined in the consensus layer -- i.e. validate that - `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`. -- _[IGNORE]_ this is the first signed bid seen with a valid signature from the - given builder for this slot. -- _[IGNORE]_ this bid is the highest value bid seen for the tuple - `(bid.slot, bid.parent_block_hash, bid.parent_block_root)`. -- _[IGNORE]_ `bid.value` is less or equal than the builder's excess balance -- - i.e. `can_builder_cover_bid(state, builder_index, amount)` returns `True`. -- _[IGNORE]_ `bid.parent_block_hash` is the block hash of a known execution - payload in fork choice and - `is_gas_limit_target_compatible(parent_gas_limit, bid.gas_limit, proposer_preferences.target_gas_limit)` - is `True` where `parent_gas_limit` is the `gas_limit` of that execution - payload. -- _[IGNORE]_ `bid.parent_block_root` is the hash tree root of a known beacon - block in fork choice. -- _[REJECT]_ The bid is for a higher slot than its parent block -- i.e. validate - that `bid.slot` is greater than the slot of the block with root - `bid.parent_block_root`. -- _[REJECT]_ `signed_execution_payload_bid.signature` is valid with respect to - the `bid.builder_index`. +*Note*: The `state` passed to `validate_execution_payload_bid_gossip` is the +bid's parent block post-state. The function advances it to the bid's slot so +that builder checks such as `is_active_builder` and `can_builder_cover_bid` are +evaluated at the bid's slot rather than at the parent's slot. ```python -def is_gas_limit_target_compatible( - parent_gas_limit: uint64, gas_limit: uint64, target_gas_limit: uint64 -) -> bool: +def validate_execution_payload_bid_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_execution_payload_bid: SignedExecutionPayloadBid, + current_time_ms: uint64, +) -> None: """ - Check if ``gas_limit`` is compatible with ``target_gas_limit`` under the - EIP-1559 transition rule from ``parent_gas_limit``. + Validate a SignedExecutionPayloadBid for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. """ - max_gas_limit_difference = max(parent_gas_limit // 1024, 1) - 1 - min_gas_limit = parent_gas_limit - max_gas_limit_difference - max_gas_limit = parent_gas_limit + max_gas_limit_difference + bid = signed_execution_payload_bid.message + + # [IGNORE] The bid's slot is the current slot or the next slot + if not is_current_or_next_slot(state, bid.slot, current_time_ms): + raise GossipIgnore("bid slot is not the current or next slot") + + # [IGNORE] This is the first bid from this builder for this slot + bid_key = (bid.builder_index, bid.slot) + if bid_key in seen.execution_payload_bids: + raise GossipIgnore("already seen valid bid from this builder for this slot") + + # [IGNORE] This is the highest value bid seen for the slot and parent + best_bid_key = (bid.slot, bid.parent_block_hash, bid.parent_block_root) + best_bid_value = seen.best_execution_payload_bid.get(best_bid_key, Gwei(0)) + if bid.value <= best_bid_value: + raise GossipIgnore("bid is not the highest value bid seen for this slot and parent") + + # [REJECT] The bid is for a higher slot than its parent block + if bid.slot <= state.slot: + raise GossipReject("bid's slot is not higher than its parent's slot") + + # Advance state + state = state.copy() + process_slots(state, bid.slot) + + # [REJECT] The builder index is valid + if bid.builder_index >= len(state.builders): + raise GossipReject("builder index out of range") + + # [IGNORE] The builder can cover the bid + if not can_builder_cover_bid(state, bid.builder_index, bid.value): + raise GossipIgnore("builder cannot cover bid value") + + # [REJECT] The bid's execution payment is zero + if bid.execution_payment != 0: + raise GossipReject("bid's execution payment must be zero") + + # [REJECT] The builder is active + if not is_active_builder(state, bid.builder_index): + raise GossipReject("builder is not active") + + # [REJECT] The bid's blob KZG commitment count is within the per-epoch limit + proposal_epoch = compute_epoch_at_slot(bid.slot) + max_blobs = get_blob_parameters(proposal_epoch).max_blobs_per_block + if len(bid.blob_kzg_commitments) > max_blobs: + raise GossipReject("too many blob kzg commitments") + + # [IGNORE] The bid's parent block root is a known beacon block + # (MAY be queued until parent is retrieved) + if bid.parent_block_root not in store.blocks: + raise GossipIgnore("bid's parent block root is not a known beacon block") + + # [IGNORE] The bid's parent block hash is the hash of a known execution payload + if bid.parent_block_hash not in seen.execution_payloads: + raise GossipIgnore("bid's parent block hash is not a known execution payload") + + # [IGNORE] The bid's parent block state has been seen + if bid.parent_block_root not in store.block_states: + raise GossipIgnore("bid's parent block state is unavailable") + + # [IGNORE] The matching proposer preferences have been seen + parent_state = store.block_states[bid.parent_block_root] + dependent_root = get_proposer_dependent_root(parent_state, proposal_epoch) + prefs_key = (dependent_root, bid.slot) + if prefs_key not in seen.proposer_preferences: + raise GossipIgnore("matching proposer preferences have not been seen") + + proposer_preferences = seen.proposer_preferences[prefs_key] + + # [REJECT] The bid's fee recipient matches the proposer's preference + if bid.fee_recipient != proposer_preferences.fee_recipient: + raise GossipReject("bid's fee recipient does not match the proposer's preference") + + # [IGNORE] The bid's gas limit is compatible with the proposer's target gas limit + parent_gas_limit = seen.execution_payloads[bid.parent_block_hash].gas_limit + if not is_gas_limit_target_compatible( + parent_gas_limit, bid.gas_limit, proposer_preferences.target_gas_limit + ): + raise GossipIgnore("bid gas limit is not compatible with the proposer's target") - if target_gas_limit >= min_gas_limit and target_gas_limit <= max_gas_limit: - return gas_limit == target_gas_limit - if target_gas_limit > max_gas_limit: - return gas_limit == max_gas_limit - return gas_limit == min_gas_limit + # [REJECT] The bid signature is valid + if not verify_execution_payload_bid_signature(state, signed_execution_payload_bid): + raise GossipReject("invalid bid signature") + + # Mark this bid as seen and update the highest-value bid for this slot/parent + seen.execution_payload_bids.add(bid_key) + seen.best_execution_payload_bid[best_bid_key] = bid.value ``` *Note*: Implementations SHOULD include DoS prevention measures to mitigate spam @@ -418,7 +828,7 @@ Possible strategies include: (1) only forwarding bids that exceed the current highest bid by a minimum threshold, or (2) forwarding only the highest observed bid at regular time intervals. -###### `proposer_preferences` +###### New `proposer_preferences` *[New in Gloas:EIP7732]* @@ -426,113 +836,277 @@ This topic is used to propagate signed proposer preferences as `SignedProposerPreferences`. These messages allow validators to communicate their preferred `fee_recipient` and `target_gas_limit` to builders. -The following validations MUST pass before forwarding the -`signed_proposer_preferences` on the network, assuming the alias -`preferences = signed_proposer_preferences.message`: - -- _[IGNORE]_ `preferences.proposal_slot` is within the proposer lookahead -- - i.e. `compute_epoch_at_slot(preferences.proposal_slot)` is in the range - `[compute_epoch_at_slot(current_slot), compute_epoch_at_slot(current_slot) + MIN_SEED_LOOKAHEAD]`. -- _[IGNORE]_ `preferences.proposal_slot` has not already passed -- i.e. - `preferences.proposal_slot > current_slot`. -- _[IGNORE]_ The block with root `preferences.dependent_root` has been seen (via - gossip or non-gossip sources) (a client MAY queue the message for - re-processing once the block is retrieved). -- _[REJECT]_ `is_valid_proposal_slot(state, preferences)` returns `True`, where - `state` is the checkpoint state at the epoch - `compute_epoch_at_slot(preferences.proposal_slot) - MIN_SEED_LOOKAHEAD` and - the root `preferences.dependent_root`. -- _[IGNORE]_ The `signed_proposer_preferences` is the first valid message seen - for the tuple - `(preferences.dependent_root, preferences.proposal_slot, preferences.validator_index)`. -- _[REJECT]_ `signed_proposer_preferences.signature` is valid with respect to - the validator's public key. +*Note*: Nodes SHOULD subscribe to this topic at least one epoch before the fork +activation. Proposers SHOULD broadcast their preferences in the epoch before the +fork. ```python -def is_valid_proposal_slot(state: BeaconState, preferences: ProposerPreferences) -> bool: +def validate_proposer_preferences_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_proposer_preferences: SignedProposerPreferences, + current_time_ms: uint64, +) -> None: """ - Check if the validator is the proposer for the given slot within the - proposer lookahead. + Validate a SignedProposerPreferences for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. """ + preferences = signed_proposer_preferences.message + + # [IGNORE] The proposal slot's epoch is at or after the current epoch current_epoch = get_current_epoch(state) proposal_epoch = compute_epoch_at_slot(preferences.proposal_slot) if proposal_epoch < current_epoch: - return False - if proposal_epoch > current_epoch + Epoch(MIN_SEED_LOOKAHEAD): - return False + raise GossipIgnore("proposal slot is before the current epoch") - index = (proposal_epoch - current_epoch) * SLOTS_PER_EPOCH - index += preferences.proposal_slot % SLOTS_PER_EPOCH - return state.proposer_lookahead[index] == preferences.validator_index + # [IGNORE] The proposal slot's epoch is within the proposer lookahead + if proposal_epoch > current_epoch + Epoch(MIN_SEED_LOOKAHEAD): + raise GossipIgnore("proposal slot is past the proposer lookahead") + + # [IGNORE] The proposal slot has not already passed + if is_not_from_future_slot(state, preferences.proposal_slot, current_time_ms): + raise GossipIgnore("proposal slot has already passed") + + # [IGNORE] The dependent block has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + if preferences.dependent_root not in store.blocks: + raise GossipIgnore("dependent root block has not been seen") + + # [IGNORE] These are the first valid preferences seen for this dependent root and slot + prefs_key = (preferences.dependent_root, preferences.proposal_slot) + if prefs_key in seen.proposer_preferences: + raise GossipIgnore("already seen preferences for this dependent root and proposal slot") + + # [IGNORE] The dependent root's state has been seen + if preferences.dependent_root not in store.block_states: + raise GossipIgnore("dependent root state is unavailable") + + # [REJECT] The validator is the proposer for the given slot in the proposer lookahead + checkpoint_state = store.block_states[preferences.dependent_root].copy() + checkpoint_epoch = Epoch(proposal_epoch - MIN_SEED_LOOKAHEAD) + process_slots(checkpoint_state, compute_start_slot_at_epoch(checkpoint_epoch)) + if not is_valid_proposal_slot(checkpoint_state, preferences): + raise GossipReject("validator is not the proposer for the given slot") + + # [REJECT] The validator index is valid + if preferences.validator_index >= len(state.validators): + raise GossipReject("validator index out of range") + + # [REJECT] The signature is valid with respect to the validator's public key + validator = state.validators[preferences.validator_index] + domain = get_domain(state, DOMAIN_PROPOSER_PREFERENCES, proposal_epoch) + signing_root = compute_signing_root(preferences, domain) + if not bls.Verify(validator.pubkey, signing_root, signed_proposer_preferences.signature): + raise GossipReject("invalid proposer preferences signature") + + # Mark these preferences as seen + seen.proposer_preferences[prefs_key] = preferences ``` +##### Blob subnets + +###### Modified `data_column_sidecar_{subnet_id}` + +The KZG commitments needed to verify a sidecar are now carried by the bid at +`block.body.signed_execution_payload_bid.message.blob_kzg_commitments`, where +`block` is the `BeaconBlock` with root `sidecar.beacon_block_root`. + +*Note*: If the sidecar fails deferred validation, its forwarding peers MUST be +downscored retroactively. If validation succeeds, the client MUST re-broadcast +the sidecar. + ```python -def get_proposer_dependent_root(state: BeaconState, epoch: Epoch) -> Root: +def validate_data_column_sidecar_gossip( + seen: Seen, + store: Store, + # [Modified in Gloas:EIP7732] + # Removed `state` + sidecar: DataColumnSidecar, + # [Modified in Gloas:EIP7732] + # Removed `current_time_ms` + subnet_id: SubnetID, +) -> None: """ - Return the dependent root for the proposer lookahead at ``epoch``. + Validate a DataColumnSidecar for gossip propagation on a subnet. + Raises GossipIgnore or GossipReject on validation failure. """ - return get_block_root_at_slot( - state, Slot(compute_start_slot_at_epoch(Epoch(epoch - MIN_SEED_LOOKAHEAD)) - 1) - ) -``` + # [IGNORE] This is the first sidecar seen for this block root and column index + sidecar_tuple = (sidecar.beacon_block_root, sidecar.index) + if sidecar_tuple in seen.data_column_sidecar_tuples: + raise GossipIgnore("already seen sidecar for this block root and index") -*Note*: Nodes SHOULD subscribe to this topic at least one epoch before the fork -activation. Proposers SHOULD broadcast their preferences in the epoch before the -fork. + # [REJECT] The sidecar is for the correct subnet + if compute_subnet_for_data_column_sidecar(sidecar.index) != subnet_id: + raise GossipReject("sidecar is for wrong subnet") -##### Blob subnets + # [IGNORE] A valid block for the sidecar has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + # (SHOULD queue at least one sidecar per peer per subnet) + if sidecar.beacon_block_root not in store.blocks: + raise GossipIgnore("block for sidecar's beacon block root has not been seen") -###### `data_column_sidecar_{subnet_id}` - -*[Modified in Gloas:EIP7732]* - -The following validations MUST pass before forwarding the -`sidecar: DataColumnSidecar` on the network, assuming the alias -`bid = block.body.signed_execution_payload_bid.message` where `block` is the -`BeaconBlock` associated with `sidecar.beacon_block_root`: - -- _[IGNORE]_ A valid block for the sidecar's `slot` has been seen (via gossip or - non-gossip sources). If not yet seen, a client SHOULD queue the sidecar for - deferred validation and possible processing once the block is received or - retrieved. A client SHOULD queue at least one sidecar per peer per subnet. -- _[REJECT]_ The sidecar's `slot` matches the slot of the block with root - `beacon_block_root`. -- _[REJECT]_ The sidecar is valid as verified by - `verify_data_column_sidecar(sidecar, bid.blob_kzg_commitments)`. -- _[REJECT]_ The sidecar is for the correct subnet -- i.e. - `compute_subnet_for_data_column_sidecar(sidecar.index) == subnet_id`. -- _[REJECT]_ The sidecar's column data is valid as verified by - `verify_data_column_sidecar_kzg_proofs(sidecar, bid.blob_kzg_commitments)`. -- _[IGNORE]_ The sidecar is the first sidecar for the tuple - `(sidecar.beacon_block_root, sidecar.index)` with valid kzg proof. + block = store.blocks[sidecar.beacon_block_root] -*Note*: If the sidecar fails deferred validation, its forwarding peers MUST be -downscored retroactively. If validation succeeds, the client MUST re-broadcast -the sidecar. + # [REJECT] The sidecar's slot matches the slot of the block + if sidecar.slot != block.slot: + raise GossipReject("sidecar's slot does not match block's slot") + + bid = block.body.signed_execution_payload_bid.message + + # [REJECT] The sidecar passes structural validation + if not verify_data_column_sidecar(sidecar, bid.blob_kzg_commitments): + raise GossipReject("invalid sidecar") + + # [REJECT] The sidecar's column data passes KZG verification + if not verify_data_column_sidecar_kzg_proofs(sidecar, bid.blob_kzg_commitments): + raise GossipReject("invalid sidecar kzg proofs") + + # Mark this data column sidecar as seen + seen.data_column_sidecar_tuples.add(sidecar_tuple) +``` ##### Attestation subnets -###### `beacon_attestation_{subnet_id}` +###### Modified `beacon_attestation_{subnet_id}` -Let `block` be the beacon block corresponding to -`attestation.data.beacon_block_root`. +*Note*: This function is modified per EIP-7732. `attestation.data.index` is now +restricted to `{0, 1}`, encoding whether the execution payload was present at +the slot. Same-slot attestations MUST attest with `index == 0`. Attestations +with `index == 1` require that the corresponding execution payload envelope has +been seen and passes execution-layer validation. -The following validations are added: +```python +def validate_beacon_attestation_gossip( + seen: Seen, + store: Store, + state: BeaconState, + attestation: SingleAttestation, + current_time_ms: uint64, + subnet_id: SubnetID, + # [New in Gloas:EIP7732] + block_payload_statuses: Dict[Root, PayloadValidationStatus], +) -> None: + """ + Validate a SingleAttestation for gossip propagation on a subnet. + Raises GossipIgnore or GossipReject on validation failure. + """ + data = attestation.data + committee_index = attestation.committee_index + attester_index = attestation.attester_index + target_epoch = data.target.epoch -- _[REJECT]_ `attestation.data.index < 2`. -- _[REJECT]_ `attestation.data.index == 0` if - `block.slot == attestation.data.slot`. -- _[REJECT]_ If `attestation.data.index == 1` (payload present for a past - block), the execution payload for `block` passes validation. -- _[IGNORE]_ When `attestation.data.index == 1` (payload present for a past - block), the execution payload for `block` has been seen (a client MAY queue - attestations for processing once the payload is retrieved and SHOULD request - the payload envelope via `ExecutionPayloadEnvelopesByRoot` using - `attestation.data.beacon_block_root`). + # [New in Gloas:EIP7732] + # [REJECT] The attestation's data index is 0 or 1 + if data.index > 1: + raise GossipReject("attestation data index must be 0 or 1") + + # [REJECT] The committee index is within the expected range + committees_per_slot = get_committee_count_per_slot(state, target_epoch) + if committee_index >= committees_per_slot: + raise GossipReject("committee index out of range") + + # [REJECT] The attestation is for the correct subnet + expected_subnet = compute_subnet_for_attestation( + committees_per_slot, data.slot, committee_index + ) + if expected_subnet != subnet_id: + raise GossipReject("attestation is for wrong subnet") + + # [IGNORE] The attestation's slot is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, data.slot, current_time_ms): + raise GossipIgnore("attestation slot is from a future slot") + + # [IGNORE] The attestation's epoch is either the current or previous epoch + attestation_epoch = compute_epoch_at_slot(data.slot) + is_previous_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(Epoch(attestation_epoch + 1)), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + is_current_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(attestation_epoch), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + if not (is_previous_epoch_attestation or is_current_epoch_attestation): + raise GossipIgnore("attestation epoch is not previous or current epoch") -The following validations are removed: + # [REJECT] The attestation's epoch matches its target + if target_epoch != compute_epoch_at_slot(data.slot): + raise GossipReject("attestation epoch does not match target epoch") -- _[REJECT]_ `attestation.data.index == 0`. + # [REJECT] The attester is a member of the committee + committee = get_beacon_committee(state, data.slot, committee_index) + if attester_index not in committee: + raise GossipReject("attester is not a member of the committee") + + # [IGNORE] No other valid attestation seen for this validator and target epoch + if (attester_index, target_epoch) in seen.attestation_validator_epochs: + raise GossipIgnore("already seen attestation from this validator for this epoch") + + # [REJECT] The attestation signature is valid + attester = state.validators[attester_index] + domain = get_domain(state, DOMAIN_BEACON_ATTESTER, target_epoch) + signing_root = compute_signing_root(data, domain) + if not bls.Verify(attester.pubkey, signing_root, attestation.signature): + raise GossipReject("invalid attestation signature") + + # [IGNORE] The block being voted for has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + beacon_block_root = data.beacon_block_root + if beacon_block_root not in store.blocks: + raise GossipIgnore("block being voted for has not been seen") + + # [REJECT] The block being voted for passes validation + if beacon_block_root not in store.block_states: + raise GossipReject("block being voted for failed validation") + + block = store.blocks[beacon_block_root] + + # [New in Gloas:EIP7732] + # [REJECT] For same-slot attestations, the payload cannot yet be present + if block.slot == data.slot and data.index != 0: + raise GossipReject("same-slot attestation must attest with index 0") + + if data.index == 1: + # [New in Gloas:EIP7732] + # [IGNORE] The corresponding execution payload envelope has been seen + # (MAY queue attestations for processing once the payload is retrieved and + # SHOULD request the payload envelope via ExecutionPayloadEnvelopesByRoot + # using data.beacon_block_root) + payload_status = block_payload_statuses.get(beacon_block_root) + if payload_status is None: + raise GossipIgnore("execution payload envelope has not been seen") + + # [New in Gloas:EIP7732] + # [IGNORE] The corresponding execution payload has been validated + if payload_status == PAYLOAD_STATUS_NOT_VALIDATED: + raise GossipIgnore("execution payload pending EL validation") + + # [New in Gloas:EIP7732] + # [REJECT] The corresponding execution payload passes EL validation + if payload_status == PAYLOAD_STATUS_INVALIDATED: + raise GossipReject("execution payload failed EL validation") + + # [REJECT] The attestation's target block is an ancestor of the LMD vote block + target_checkpoint_block = get_checkpoint_block(store, beacon_block_root, target_epoch) + if target_checkpoint_block != data.target.root: + raise GossipReject("target block is not an ancestor of LMD vote block") + + # [IGNORE] The current finalized checkpoint is an ancestor of the block + finalized_checkpoint_block = get_checkpoint_block( + store, beacon_block_root, store.finalized_checkpoint.epoch + ) + if finalized_checkpoint_block != store.finalized_checkpoint.root: + raise GossipIgnore("finalized checkpoint is not an ancestor of block") + + # Mark this attestation as seen + seen.attestation_validator_epochs.add((attester_index, target_epoch)) +``` ### The Req/Resp domain diff --git a/specs/gloas/partial-columns/p2p-interface.md b/specs/gloas/partial-columns/p2p-interface.md index 7b1b1392ae..2ab286d30c 100644 --- a/specs/gloas/partial-columns/p2p-interface.md +++ b/specs/gloas/partial-columns/p2p-interface.md @@ -5,12 +5,11 @@ - [Introduction](#introduction) -- [Modification in Gloas](#modification-in-gloas) - - [Containers](#containers) - - [Modified `PartialDataColumnSidecar`](#modified-partialdatacolumnsidecar) - - [New `PartialDataColumnGroupID`](#new-partialdatacolumngroupid) - - [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) - - [Partial Messages on `data_column_sidecar_{subnet_id}`](#partial-messages-on-data_column_sidecar_subnet_id) +- [Containers](#containers) + - [Modified `PartialDataColumnSidecar`](#modified-partialdatacolumnsidecar) + - [Modified `PartialDataColumnGroupID`](#modified-partialdatacolumngroupid) +- [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) + - [Modified `data_column_sidecar_{subnet_id}` (partial messages)](#modified-data_column_sidecar_subnet_id-partial-messages) @@ -25,11 +24,9 @@ particular, this document builds on the [Fulu partial columns networking specification](../../fulu/partial-columns/p2p-interface.md) and the [Gloas networking specification](../p2p-interface.md). -## Modification in Gloas +## Containers -### Containers - -#### Modified `PartialDataColumnSidecar` +### Modified `PartialDataColumnSidecar` ```python class PartialDataColumnSidecar(Container): @@ -40,84 +37,81 @@ class PartialDataColumnSidecar(Container): # Removed `header` ``` -#### New `PartialDataColumnGroupID` +### Modified `PartialDataColumnGroupID` ```python class PartialDataColumnGroupID(Container): - slot: Slot beacon_block_root: Root + # [New in Gloas:EIP7732] + slot: Slot ``` -### The gossip domain: gossipsub - -#### Partial Messages on `data_column_sidecar_{subnet_id}` - -*[Modified in Gloas:EIP7732]* - -*Note*: The Partial Message Group ID is the SSZ encoded -`PartialDataColumnGroupID` prefixed with the version byte `0x01`. -Implementations MUST ignore unknown versions. - -**Added in Gloas:** - -*Note*: The added rules are similar to the changes in validation rules for full -messages on `data_column_sidecar_{subnet_id}` as defined above. - -- _[IGNORE]_ A valid block for the Group ID's `slot` has been seen (via gossip - or non-gossip sources). If not yet seen, a client SHOULD queue the sidecar for - deferred validation and possible processing once the block is received or - retrieved. A client SHOULD queue at least 1 sidecar per peer per subnet. -- _[REJECT]_ The Group ID's `slot` matches the slot of the block with root - `beacon_block_root`. The `beacon_block_root` is also identified by the Group - ID. - -**Modified in Gloas:** - -*Note*: These modifications only replace the mention of the header with the bid, -as the bid contains the KZG commitments. - -- _[REJECT]_ The cells present bitmap length is equal to the number of KZG - commitments in `bid.blob_kzg_commitments`. -- _[REJECT]_ The sidecar's cell and proof data is valid as verified by - `verify_partial_data_column_sidecar_kzg_proofs(sidecar, bid.blob_kzg_commitments, column_index)`. - -**Removed from Fulu:** - -- _[REJECT]_ If a valid header was previously received, the received header MUST - equal the previously valid header. -- _[REJECT]_ The hash of the block header in `signed_block_header` MUST be the - same one identified by the partial message's group id. -- _[REJECT]_ The header's `kzg_commitments` list is non-empty. -- _[IGNORE]_ The header is not from a future slot (with a - `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- i.e. validate that - `block_header.slot <= current_slot` (a client MAY queue future headers for - processing at the appropriate slot). -- _[IGNORE]_ The header is from a slot greater than the latest finalized slot -- - i.e. validate that - `block_header.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch)` -- _[REJECT]_ The proposer signature of `signed_block_header` is valid with - respect to the `block_header.proposer_index` pubkey. -- _[IGNORE]_ The header's block's parent (defined by `block_header.parent_root`) - has been seen (via gossip or non-gossip sources) (a client MAY queue header - for processing once the parent block is retrieved). -- _[REJECT]_ The header's block's parent (defined by `block_header.parent_root`) - passes validation. -- _[REJECT]_ The header is from a higher slot than the header's block's parent - (defined by `block_header.parent_root`). -- _[REJECT]_ The current `finalized_checkpoint` is an ancestor of the header's - block -- i.e. - `get_checkpoint_block(store, block_header.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root`. -- _[REJECT]_ The header's `kzg_commitments` field inclusion proof is valid as - verified by `verify_partial_data_column_header_inclusion_proof`. -- _[REJECT]_ The header is proposed by the expected `proposer_index` for the - block's slot in the context of the current shuffling (defined by - `block_header.parent_root`/`block_header.slot`). If the `proposer_index` - cannot immediately be verified against the expected shuffling, the header MAY - be queued for later processing while proposers for the block's branch are - calculated -- in such a case _do not_ `REJECT`, instead `IGNORE` this message. -- _[IGNORE]_ If the received partial message contains only cell and proof data, - the node has seen a valid corresponding `PartialDataColumnHeader`. -- _[IGNORE]_ The corresponding header is not from a future slot. See related - header check above for more details. -- _[IGNORE]_ The corresponding header is from a slot greater than the latest - finalized slot. See related header check above for more details. +## The gossip domain: gossipsub + +### Modified `data_column_sidecar_{subnet_id}` (partial messages) + +*Note*: The KZG commitments needed to verify a partial sidecar are now carried +by the bid at +`block.body.signed_execution_payload_bid.message.blob_kzg_commitments`, where +`block` is the `BeaconBlock` with root `group_id.beacon_block_root`. All +header-related validations from Fulu are removed; their role is taken over by +the bid commitments and the corresponding block validation. + +```python +def validate_partial_data_column_sidecar_gossip( + # [Modified in Gloas:EIP7732] + # Removed `seen` + store: Store, + # [Modified in Gloas:EIP7732] + # Removed `state` + sidecar: PartialDataColumnSidecar, + # [Modified in Gloas:EIP7732] + # Removed `current_time_ms` + group_id: PartialDataColumnGroupID, + column_index: ColumnIndex, +) -> None: + """ + Validate a PartialDataColumnSidecar for gossip propagation on a subnet. + Raises GossipIgnore or GossipReject on validation failure. + """ + num_cells_present = sum(1 for b in sidecar.cells_present_bitmap if b) + + # [Modified in Gloas] + # [REJECT] The message contains at least one cell + if num_cells_present == 0: + raise GossipReject("partial message is semantically empty") + + # [REJECT] The cell count equals the number of set bits in the bitmap + if len(sidecar.partial_column) != num_cells_present: + raise GossipReject("number of cells does not match number of set bits") + + # [REJECT] The proof count equals the number of set bits in the bitmap + if len(sidecar.kzg_proofs) != num_cells_present: + raise GossipReject("number of proofs does not match number of set bits") + + # [New in Gloas] + # [IGNORE] The group ID's block has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + # (SHOULD queue at least one sidecar per peer per subnet) + if group_id.beacon_block_root not in store.blocks: + raise GossipIgnore("group id's beacon block has not been seen") + + block = store.blocks[group_id.beacon_block_root] + + # [New in Gloas] + # [REJECT] The group ID's slot matches the slot of the block + if group_id.slot != block.slot: + raise GossipReject("group id's slot does not match the block's slot") + + bid = block.body.signed_execution_payload_bid.message + + # [REJECT] The cells present bitmap length equals the number of bid commitments + if len(sidecar.cells_present_bitmap) != len(bid.blob_kzg_commitments): + raise GossipReject("bitmap length does not match the number of bid commitments") + + # [REJECT] The sidecar's cell and proof data passes KZG verification + if not verify_partial_data_column_sidecar_kzg_proofs( + sidecar, bid.blob_kzg_commitments, column_index + ): + raise GossipReject("invalid sidecar kzg proofs") +``` diff --git a/tests/core/pyspec/eth_consensus_specs/test/altair/networking/test_gossip_sync_committee_contribution_and_proof.py b/tests/core/pyspec/eth_consensus_specs/test/altair/networking/test_gossip_sync_committee_contribution_and_proof.py index 94f40f6ab5..54f310b536 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/altair/networking/test_gossip_sync_committee_contribution_and_proof.py +++ b/tests/core/pyspec/eth_consensus_specs/test/altair/networking/test_gossip_sync_committee_contribution_and_proof.py @@ -5,7 +5,7 @@ with_presets, ) from eth_consensus_specs.test.helpers.constants import MAINNET -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen +from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, run_validate_gossip from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.utils import bls @@ -119,24 +119,6 @@ def create_valid_signed_contribution_and_proof( ) -def run_validate_contribution_gossip( - spec, seen, state, signed_contribution_and_proof, current_time_ms -): - """Run validate_sync_committee_contribution_and_proof_gossip and return the result.""" - try: - spec.validate_sync_committee_contribution_and_proof_gossip( - seen, - state, - signed_contribution_and_proof, - current_time_ms, - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - @with_altair_and_later @spec_state_test @always_bls @@ -164,12 +146,12 @@ def test_gossip_sync_committee_contribution_and_proof__valid(spec, state): yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms + 500, ) assert result == "valid" assert reason is None @@ -214,12 +196,12 @@ def test_gossip_sync_committee_contribution_and_proof__valid_at_period_boundary( yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms + 500, ) assert result == "valid" assert reason is None @@ -259,12 +241,12 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_future_slot(spec, yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms, ) assert result == "ignore" assert reason == "contribution is not for the current slot" @@ -315,12 +297,12 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_past_slot(spec, st yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms, ) assert result == "ignore" assert reason == "contribution is not for the current slot" @@ -370,12 +352,12 @@ def test_gossip_sync_committee_contribution_and_proof__reject_invalid_subcommitt yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms + 500, ) assert result == "reject" assert reason == "subcommittee index out of range" @@ -424,12 +406,12 @@ def test_gossip_sync_committee_contribution_and_proof__reject_no_participants(sp yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms + 500, ) assert result == "reject" assert reason == "contribution has no participants" @@ -495,12 +477,12 @@ def test_gossip_sync_committee_contribution_and_proof__reject_not_aggregator(spe yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms + 500, ) assert result == "reject" assert reason == "validator is not selected as aggregator" @@ -553,12 +535,12 @@ def test_gossip_sync_committee_contribution_and_proof__reject_aggregator_not_in_ yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms + 500, ) assert result == "reject" assert reason == "aggregator not in subcommittee" @@ -607,12 +589,12 @@ def test_gossip_sync_committee_contribution_and_proof__reject_aggregator_index_o yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms + 500, ) assert result == "reject" assert reason == "aggregator index out of range" @@ -709,12 +691,12 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_superset_contribut yield "current_time_ms", "meta", int(current_time_ms) # First: superset passes - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_superset, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_superset, + current_time_ms=current_time_ms + 500, ) assert result == "valid" assert reason is None @@ -734,12 +716,12 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_superset_contribut yield get_filename(signed_subset), signed_subset - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_subset, - current_time_ms + 600, + seen=seen, + state=state, + signed_contribution_and_proof=signed_subset, + current_time_ms=current_time_ms + 600, ) assert result == "ignore" assert reason == "already seen contribution for this data" @@ -806,12 +788,12 @@ def test_gossip_sync_committee_contribution_and_proof__valid_non_superset_contri yield get_filename(signed_subset), signed_subset - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_subset, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_subset, + current_time_ms=current_time_ms + 500, ) assert result == "valid" assert reason is None @@ -858,12 +840,12 @@ def test_gossip_sync_committee_contribution_and_proof__valid_non_superset_contri yield get_filename(signed_superset), signed_superset # Superset has new bits → is_non_strict_superset=False, passes the check → valid - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_superset, - current_time_ms + 600, + seen=seen, + state=state, + signed_contribution_and_proof=signed_superset, + current_time_ms=current_time_ms + 600, ) assert result == "valid" assert reason is None @@ -905,12 +887,12 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_duplicate_aggregat yield "current_time_ms", "meta", int(current_time_ms) # First validation should pass - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap1, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap1, + current_time_ms=current_time_ms + 500, ) assert result == "valid" assert reason is None @@ -930,12 +912,12 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_duplicate_aggregat yield get_filename(signed_cap2), signed_cap2 - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap2, - current_time_ms + 600, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap2, + current_time_ms=current_time_ms + 600, ) assert result == "ignore" assert reason == "already seen contribution from this aggregator" @@ -988,12 +970,12 @@ def test_gossip_sync_committee_contribution_and_proof__reject_invalid_selection_ yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms + 500, ) assert result == "reject" assert reason == "invalid selection proof signature" @@ -1048,12 +1030,12 @@ def test_gossip_sync_committee_contribution_and_proof__reject_invalid_aggregator yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms + 500, ) assert result == "reject" assert reason == "invalid aggregator signature" @@ -1115,12 +1097,12 @@ def test_gossip_sync_committee_contribution_and_proof__reject_invalid_aggregate_ yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_contribution_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - signed_cap, - current_time_ms + 500, + seen=seen, + state=state, + signed_contribution_and_proof=signed_cap, + current_time_ms=current_time_ms + 500, ) assert result == "reject" assert reason == "invalid aggregate signature" diff --git a/tests/core/pyspec/eth_consensus_specs/test/altair/networking/test_gossip_sync_committee_message.py b/tests/core/pyspec/eth_consensus_specs/test/altair/networking/test_gossip_sync_committee_message.py index c57a8b99e6..bdb162f12e 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/altair/networking/test_gossip_sync_committee_message.py +++ b/tests/core/pyspec/eth_consensus_specs/test/altair/networking/test_gossip_sync_committee_message.py @@ -3,7 +3,7 @@ spec_state_test, with_altair_and_later, ) -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen +from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, run_validate_gossip from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.utils import bls @@ -37,25 +37,6 @@ def create_valid_sync_committee_message(spec, state, validator_index, slot=None, ) -def run_validate_sync_committee_message_gossip( - spec, seen, state, message, subnet_id, current_time_ms -): - """Run validate_sync_committee_message_gossip and return the result.""" - try: - spec.validate_sync_committee_message_gossip( - seen, - state, - message, - current_time_ms, - subnet_id, - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - @with_altair_and_later @spec_state_test @always_bls @@ -74,13 +55,13 @@ def test_gossip_sync_committee_message__valid(spec, state): yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_sync_committee_message_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - message, - subnet_id, - current_time_ms + 500, + seen=seen, + state=state, + sync_committee_message=message, + current_time_ms=current_time_ms + 500, + subnet_id=subnet_id, ) assert result == "valid" assert reason is None @@ -119,13 +100,13 @@ def test_gossip_sync_committee_message__ignore_future_slot(spec, state): yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_sync_committee_message_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - message, - subnet_id, - current_time_ms, + seen=seen, + state=state, + sync_committee_message=message, + current_time_ms=current_time_ms, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "message is not for the current slot" @@ -169,13 +150,13 @@ def test_gossip_sync_committee_message__ignore_past_slot(spec, state): yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_sync_committee_message_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - message, - subnet_id, - current_time_ms, + seen=seen, + state=state, + sync_committee_message=message, + current_time_ms=current_time_ms, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "message is not for the current slot" @@ -215,13 +196,13 @@ def test_gossip_sync_committee_message__reject_wrong_subnet(spec, state): # Use a wrong subnet_id wrong_subnet_id = (correct_subnet_id + 1) % spec.SYNC_COMMITTEE_SUBNET_COUNT - result, reason = run_validate_sync_committee_message_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - message, - wrong_subnet_id, - current_time_ms + 500, + seen=seen, + state=state, + sync_committee_message=message, + current_time_ms=current_time_ms + 500, + subnet_id=wrong_subnet_id, ) assert result == "reject" assert reason == "subnet_id is not valid for the validator" @@ -260,13 +241,13 @@ def test_gossip_sync_committee_message__reject_validator_index_out_of_range(spec yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_sync_committee_message_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - message, - subnet_id, - current_time_ms + 500, + seen=seen, + state=state, + sync_committee_message=message, + current_time_ms=current_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "validator index out of range" @@ -305,13 +286,13 @@ def test_gossip_sync_committee_message__ignore_duplicate(spec, state): yield "current_time_ms", "meta", int(current_time_ms) # First validation should pass - result, reason = run_validate_sync_committee_message_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - message, - subnet_id, - current_time_ms + 500, + seen=seen, + state=state, + sync_committee_message=message, + current_time_ms=current_time_ms + 500, + subnet_id=subnet_id, ) assert result == "valid" assert reason is None @@ -325,13 +306,13 @@ def test_gossip_sync_committee_message__ignore_duplicate(spec, state): ) # Second validation should be ignored - result, reason = run_validate_sync_committee_message_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - message, - subnet_id, - current_time_ms + 600, + seen=seen, + state=state, + sync_committee_message=message, + current_time_ms=current_time_ms + 600, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "already seen message from this validator for this slot and subnet" @@ -381,13 +362,13 @@ def test_gossip_sync_committee_message__reject_invalid_signature(spec, state): yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_sync_committee_message_gossip( + result, reason = run_validate_gossip( spec, - seen, - state, - message, - subnet_id, - current_time_ms + 500, + seen=seen, + state=state, + sync_committee_message=message, + current_time_ms=current_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "invalid sync committee message signature" diff --git a/tests/core/pyspec/eth_consensus_specs/test/bellatrix/networking/test_gossip_beacon_block.py b/tests/core/pyspec/eth_consensus_specs/test/bellatrix/networking/test_gossip_beacon_block.py index eeb23e6045..6d48970f95 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/bellatrix/networking/test_gossip_beacon_block.py +++ b/tests/core/pyspec/eth_consensus_specs/test/bellatrix/networking/test_gossip_beacon_block.py @@ -1,12 +1,14 @@ from eth_consensus_specs.test.context import ( spec_state_test, - with_phases, + with_all_phases_from_to, + with_bellatrix_and_later, + with_bellatrix_only, ) from eth_consensus_specs.test.helpers.block import ( build_empty_block_for_next_slot, sign_block, ) -from eth_consensus_specs.test.helpers.constants import BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU +from eth_consensus_specs.test.helpers.constants import BELLATRIX, GLOAS from eth_consensus_specs.test.helpers.execution_payload import ( build_empty_execution_payload, build_state_with_complete_transition, @@ -15,13 +17,15 @@ from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) +from eth_consensus_specs.test.helpers.forks import is_post_gloas from eth_consensus_specs.test.helpers.gossip import ( get_filename, get_seen, + get_spec_block_payload_statuses, PAYLOAD_STATUS_INVALIDATED, PAYLOAD_STATUS_NOT_VALIDATED, PAYLOAD_STATUS_VALID, - run_validate_beacon_block_gossip, + run_validate_gossip, wrap_genesis_block, ) from eth_consensus_specs.test.helpers.state import ( @@ -29,7 +33,7 @@ ) -@with_phases([BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_bellatrix_and_later @spec_state_test def test_gossip_beacon_block__valid_execution_enabled(spec, state): """ @@ -56,8 +60,17 @@ def test_gossip_beacon_block__valid_execution_enabled(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = {} + if not is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "valid" assert reason is None @@ -69,7 +82,7 @@ def test_gossip_beacon_block__valid_execution_enabled(spec, state): ) -@with_phases([BELLATRIX]) +@with_bellatrix_only @spec_state_test def test_gossip_beacon_block__valid_execution_disabled(spec, state): """ @@ -97,8 +110,14 @@ def test_gossip_beacon_block__valid_execution_disabled(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + block_payload_statuses={}, ) assert result == "valid" assert reason is None @@ -110,7 +129,7 @@ def test_gossip_beacon_block__valid_execution_disabled(spec, state): ) -@with_phases([BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_gossip_beacon_block__reject_incorrect_execution_payload_timestamp(spec, state): """ @@ -141,8 +160,14 @@ def test_gossip_beacon_block__reject_incorrect_execution_payload_timestamp(spec, yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + block_payload_statuses={}, ) assert result == "reject" assert reason == "incorrect execution payload timestamp" @@ -161,7 +186,7 @@ def test_gossip_beacon_block__reject_incorrect_execution_payload_timestamp(spec, ) -@with_phases([BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_gossip_beacon_block__reject_parent_consensus_failed_execution_not_verified(spec, state): """ @@ -222,16 +247,17 @@ def test_gossip_beacon_block__reject_parent_consensus_failed_execution_not_verif yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( + result, reason = run_validate_gossip( spec, - seen, - store, - state, - signed_child, - block_time_ms + 500, - block_payload_statuses={ - signed_block.message.hash_tree_root(): PAYLOAD_STATUS_NOT_VALIDATED, - }, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_child, + current_time_ms=block_time_ms + 500, + block_payload_statuses=get_spec_block_payload_statuses( + spec, + {signed_block.message.hash_tree_root(): PAYLOAD_STATUS_NOT_VALIDATED}, + ), ) assert result == "reject" assert reason == "block's parent is invalid and EL result is unknown" @@ -250,7 +276,7 @@ def test_gossip_beacon_block__reject_parent_consensus_failed_execution_not_verif ) -@with_phases([BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_gossip_beacon_block__ignore_parent_consensus_failed_execution_known(spec, state): """ @@ -312,14 +338,14 @@ def test_gossip_beacon_block__ignore_parent_consensus_failed_execution_known(spe yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( + result, reason = run_validate_gossip( spec, - seen, - store, - state, - signed_child, - block_time_ms + 500, - block_payload_statuses=block_payload_statuses, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_child, + current_time_ms=block_time_ms + 500, + block_payload_statuses=get_spec_block_payload_statuses(spec, block_payload_statuses), ) assert result == "ignore" assert reason == "block's parent is invalid and EL result is known" @@ -338,7 +364,7 @@ def test_gossip_beacon_block__ignore_parent_consensus_failed_execution_known(spe ) -@with_phases([BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_gossip_beacon_block__ignore_parent_execution_verified_invalid(spec, state): """ @@ -402,14 +428,14 @@ def test_gossip_beacon_block__ignore_parent_execution_verified_invalid(spec, sta yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( + result, reason = run_validate_gossip( spec, - seen, - store, - state, - signed_child, - block_time_ms + 500, - block_payload_statuses=block_payload_statuses, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_child, + current_time_ms=block_time_ms + 500, + block_payload_statuses=get_spec_block_payload_statuses(spec, block_payload_statuses), ) assert result == "ignore" assert reason == "block's parent is valid and EL result is invalid" @@ -428,7 +454,7 @@ def test_gossip_beacon_block__ignore_parent_execution_verified_invalid(spec, sta ) -@with_phases([BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_gossip_beacon_block__valid_parent_execution_verified_valid(spec, state): """ @@ -491,14 +517,14 @@ def test_gossip_beacon_block__valid_parent_execution_verified_valid(spec, state) yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( + result, reason = run_validate_gossip( spec, - seen, - store, - state, - signed_child, - block_time_ms + 500, - block_payload_statuses=block_payload_statuses, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_child, + current_time_ms=block_time_ms + 500, + block_payload_statuses=get_spec_block_payload_statuses(spec, block_payload_statuses), ) assert result == "valid" assert reason is None @@ -510,7 +536,7 @@ def test_gossip_beacon_block__valid_parent_execution_verified_valid(spec, state) ) -@with_phases([BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_gossip_beacon_block__valid_parent_optimistic(spec, state): """ @@ -573,14 +599,14 @@ def test_gossip_beacon_block__valid_parent_optimistic(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( + result, reason = run_validate_gossip( spec, - seen, - store, - state, - signed_child, - block_time_ms + 500, - block_payload_statuses=block_payload_statuses, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_child, + current_time_ms=block_time_ms + 500, + block_payload_statuses=get_spec_block_payload_statuses(spec, block_payload_statuses), ) assert result == "valid" assert reason is None diff --git a/tests/core/pyspec/eth_consensus_specs/test/capella/networking/test_gossip_bls_to_execution_change.py b/tests/core/pyspec/eth_consensus_specs/test/capella/networking/test_gossip_bls_to_execution_change.py index ce763d8e1b..34e53dc5a3 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/capella/networking/test_gossip_bls_to_execution_change.py +++ b/tests/core/pyspec/eth_consensus_specs/test/capella/networking/test_gossip_bls_to_execution_change.py @@ -8,29 +8,10 @@ get_signed_address_change as get_signed_bls_to_execution_change, ) from eth_consensus_specs.test.helpers.constants import CAPELLA -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen +from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, run_validate_gossip from eth_consensus_specs.test.helpers.keys import pubkeys -def run_validate_bls_to_execution_change_gossip( - spec, seen, state, signed_bls_to_execution_change, current_time_ms -): - """ - Run validate_bls_to_execution_change_gossip and return the result. - Returns: tuple of (result, reason) where result is "valid", "ignore", or "reject" - and reason is the exception message (or None for valid). - """ - try: - spec.validate_bls_to_execution_change_gossip( - seen, state, signed_bls_to_execution_change, current_time_ms - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - def get_capella_fork_time_ms(spec, state): """ Return the current time in milliseconds at the Capella fork epoch. @@ -55,8 +36,12 @@ def test_gossip_bls_to_execution_change__valid(spec, state): yield get_filename(signed_bls_to_execution_change), signed_bls_to_execution_change yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_bls_to_execution_change_gossip( - spec, seen, state, signed_bls_to_execution_change, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + state=state, + signed_bls_to_execution_change=signed_bls_to_execution_change, + current_time_ms=current_time_ms, ) assert result == "valid" assert reason is None @@ -90,8 +75,12 @@ def test_gossip_bls_to_execution_change__ignore_pre_capella(spec, state): yield get_filename(signed_bls_to_execution_change), signed_bls_to_execution_change yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_bls_to_execution_change_gossip( - spec, seen, state, signed_bls_to_execution_change, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + state=state, + signed_bls_to_execution_change=signed_bls_to_execution_change, + current_time_ms=current_time_ms, ) assert result == "ignore" assert reason == "current epoch is pre-capella" @@ -127,8 +116,12 @@ def test_gossip_bls_to_execution_change__ignore_already_seen(spec, state): yield get_filename(signed_bls_to_execution_change), signed_bls_to_execution_change yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_bls_to_execution_change_gossip( - spec, seen, state, signed_bls_to_execution_change, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + state=state, + signed_bls_to_execution_change=signed_bls_to_execution_change, + current_time_ms=current_time_ms, ) assert result == "valid" assert reason is None @@ -140,8 +133,12 @@ def test_gossip_bls_to_execution_change__ignore_already_seen(spec, state): } ) - result, reason = run_validate_bls_to_execution_change_gossip( - spec, seen, state, signed_bls_to_execution_change, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + state=state, + signed_bls_to_execution_change=signed_bls_to_execution_change, + current_time_ms=current_time_ms, ) assert result == "ignore" assert reason == "already seen BLS to execution change for this validator" @@ -175,8 +172,12 @@ def test_gossip_bls_to_execution_change__reject_validator_index_out_of_range(spe yield get_filename(signed_bls_to_execution_change), signed_bls_to_execution_change yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_bls_to_execution_change_gossip( - spec, seen, state, signed_bls_to_execution_change, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + state=state, + signed_bls_to_execution_change=signed_bls_to_execution_change, + current_time_ms=current_time_ms, ) assert result == "reject" assert reason == "validator index out of range" @@ -216,8 +217,12 @@ def test_gossip_bls_to_execution_change__reject_not_bls_credentials(spec, state) yield get_filename(signed_bls_to_execution_change), signed_bls_to_execution_change yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_bls_to_execution_change_gossip( - spec, seen, state, signed_bls_to_execution_change, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + state=state, + signed_bls_to_execution_change=signed_bls_to_execution_change, + current_time_ms=current_time_ms, ) assert result == "reject" assert reason == "validator does not have BLS withdrawal credentials" @@ -258,8 +263,12 @@ def test_gossip_bls_to_execution_change__reject_pubkey_mismatch(spec, state): yield get_filename(signed_bls_to_execution_change), signed_bls_to_execution_change yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_bls_to_execution_change_gossip( - spec, seen, state, signed_bls_to_execution_change, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + state=state, + signed_bls_to_execution_change=signed_bls_to_execution_change, + current_time_ms=current_time_ms, ) assert result == "reject" assert reason == "pubkey does not match validator withdrawal credentials" @@ -296,8 +305,12 @@ def test_gossip_bls_to_execution_change__reject_bad_signature(spec, state): yield get_filename(signed_bls_to_execution_change), signed_bls_to_execution_change yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_bls_to_execution_change_gossip( - spec, seen, state, signed_bls_to_execution_change, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + state=state, + signed_bls_to_execution_change=signed_bls_to_execution_change, + current_time_ms=current_time_ms, ) assert result == "reject" assert reason == "invalid BLS to execution change signature" diff --git a/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_aggregate_and_proof.py b/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_aggregate_and_proof.py index 3430a5ebf0..579dadf698 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_aggregate_and_proof.py +++ b/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_aggregate_and_proof.py @@ -1,15 +1,20 @@ from eth_consensus_specs.test.context import ( spec_state_test, - with_phases, + with_deneb_and_later, ) from eth_consensus_specs.test.helpers.attestations import ( get_valid_attestation, ) -from eth_consensus_specs.test.helpers.constants import DENEB, ELECTRA, FULU from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, wrap_genesis_block +from eth_consensus_specs.test.helpers.forks import is_post_gloas +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.test.helpers.state import transition_to @@ -39,20 +44,6 @@ def create_signed_aggregate_and_proof(spec, state, attestation): return spec.SignedAggregateAndProof(message=aggregate_and_proof, signature=signature) -def run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_aggregate_and_proof, current_time_ms -): - try: - spec.validate_beacon_aggregate_and_proof_gossip( - seen, store, state, signed_aggregate_and_proof, current_time_ms - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - def build_signed_aggregate_and_proof(spec, state, beacon_block_root): attestation = get_valid_attestation( spec, state, signed=True, beacon_block_root=beacon_block_root @@ -96,7 +87,7 @@ def build_message(signed_agg, current_time_ms, offset_ms, expected, reason=None) return message -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__accepts_one_millisecond_before_slot_start(spec, state): """Test that an aggregate is accepted one millisecond before its slot starts.""" @@ -114,8 +105,17 @@ def test_gossip_beacon_aggregate_and_proof__accepts_one_millisecond_before_slot_ yield "current_time_ms", "meta", int(current_time_ms) seen = get_seen(spec) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "valid" assert reason is None @@ -123,7 +123,7 @@ def test_gossip_beacon_aggregate_and_proof__accepts_one_millisecond_before_slot_ yield "messages", "meta", [build_message(signed_agg, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__accepts_at_slot_start(spec, state): """Test that an aggregate is accepted exactly at its slot start.""" @@ -139,8 +139,17 @@ def test_gossip_beacon_aggregate_and_proof__accepts_at_slot_start(spec, state): yield "current_time_ms", "meta", int(current_time_ms) seen = get_seen(spec) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "valid" assert reason is None @@ -148,7 +157,7 @@ def test_gossip_beacon_aggregate_and_proof__accepts_at_slot_start(spec, state): yield "messages", "meta", [build_message(signed_agg, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignores_first_slot_before_epoch_window_opens( spec, state @@ -172,8 +181,17 @@ def test_gossip_beacon_aggregate_and_proof__ignores_first_slot_before_epoch_wind yield "current_time_ms", "meta", int(current_time_ms) seen = get_seen(spec) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "ignore" assert reason == "aggregate slot is from a future slot" @@ -181,7 +199,7 @@ def test_gossip_beacon_aggregate_and_proof__ignores_first_slot_before_epoch_wind yield "messages", "meta", [build_message(signed_agg, current_time_ms, 0, "ignore", reason)] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__accepts_first_slot_when_epoch_window_opens(spec, state): """Test that a first-slot aggregate is accepted when the Deneb epoch window opens.""" @@ -200,8 +218,17 @@ def test_gossip_beacon_aggregate_and_proof__accepts_first_slot_when_epoch_window yield "current_time_ms", "meta", int(current_time_ms) seen = get_seen(spec) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "valid" assert reason is None @@ -209,7 +236,7 @@ def test_gossip_beacon_aggregate_and_proof__accepts_first_slot_when_epoch_window yield "messages", "meta", [build_message(signed_agg, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__accepts_first_slot_when_epoch_window_closes( spec, state @@ -230,8 +257,17 @@ def test_gossip_beacon_aggregate_and_proof__accepts_first_slot_when_epoch_window yield "current_time_ms", "meta", int(current_time_ms) seen = get_seen(spec) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "valid" assert reason is None @@ -239,7 +275,7 @@ def test_gossip_beacon_aggregate_and_proof__accepts_first_slot_when_epoch_window yield "messages", "meta", [build_message(signed_agg, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignores_first_slot_after_epoch_window_closes( spec, state @@ -260,8 +296,17 @@ def test_gossip_beacon_aggregate_and_proof__ignores_first_slot_after_epoch_windo yield "current_time_ms", "meta", int(current_time_ms) seen = get_seen(spec) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "ignore" assert reason == "aggregate epoch is not previous or current epoch" @@ -269,7 +314,7 @@ def test_gossip_beacon_aggregate_and_proof__ignores_first_slot_after_epoch_windo yield "messages", "meta", [build_message(signed_agg, current_time_ms, 0, "ignore", reason)] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__accepts_last_slot_one_millisecond_before_slot_start( spec, state @@ -298,8 +343,17 @@ def test_gossip_beacon_aggregate_and_proof__accepts_last_slot_one_millisecond_be yield "current_time_ms", "meta", int(current_time_ms) seen = get_seen(spec) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "valid" assert reason is None @@ -307,7 +361,7 @@ def test_gossip_beacon_aggregate_and_proof__accepts_last_slot_one_millisecond_be yield "messages", "meta", [build_message(signed_agg, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__accepts_last_slot_at_slot_start(spec, state): """Test that a last-slot aggregate is accepted exactly at its slot start.""" @@ -329,8 +383,17 @@ def test_gossip_beacon_aggregate_and_proof__accepts_last_slot_at_slot_start(spec yield "current_time_ms", "meta", int(current_time_ms) seen = get_seen(spec) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "valid" assert reason is None @@ -338,7 +401,7 @@ def test_gossip_beacon_aggregate_and_proof__accepts_last_slot_at_slot_start(spec yield "messages", "meta", [build_message(signed_agg, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__accepts_last_slot_when_epoch_window_closes(spec, state): """Test that a last-slot aggregate is accepted at the last valid Deneb epoch time.""" @@ -360,8 +423,17 @@ def test_gossip_beacon_aggregate_and_proof__accepts_last_slot_when_epoch_window_ yield "current_time_ms", "meta", int(current_time_ms) seen = get_seen(spec) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "valid" assert reason is None @@ -369,7 +441,7 @@ def test_gossip_beacon_aggregate_and_proof__accepts_last_slot_when_epoch_window_ yield "messages", "meta", [build_message(signed_agg, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignores_last_slot_after_epoch_window_closes( spec, state @@ -393,8 +465,17 @@ def test_gossip_beacon_aggregate_and_proof__ignores_last_slot_after_epoch_window yield "current_time_ms", "meta", int(current_time_ms) seen = get_seen(spec) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "ignore" assert reason == "aggregate epoch is not previous or current epoch" diff --git a/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_attestation.py b/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_attestation.py index d49f2f3e99..1613d2a8bd 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_attestation.py +++ b/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_attestation.py @@ -1,17 +1,21 @@ from eth_consensus_specs.test.context import ( spec_state_test, - with_phases, + with_deneb_and_later, ) from eth_consensus_specs.test.helpers.attestations import ( get_valid_attestation, to_single_attestation, ) -from eth_consensus_specs.test.helpers.constants import DENEB, ELECTRA, FULU from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) -from eth_consensus_specs.test.helpers.forks import is_post_electra -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, wrap_genesis_block +from eth_consensus_specs.test.helpers.forks import is_post_electra, is_post_gloas +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.test.helpers.state import transition_to @@ -26,20 +30,6 @@ def get_correct_subnet_for_attestation(spec, state, attestation): ) -def run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms -): - try: - spec.validate_beacon_attestation_gossip( - seen, store, state, attestation, current_time_ms, subnet_id - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - def build_unaggregated_attestation(spec, state, beacon_block_root): attestation = get_valid_attestation( spec, state, signed=False, beacon_block_root=beacon_block_root @@ -93,7 +83,7 @@ def build_message(attestation, subnet_id, current_time_ms, offset_ms, expected, return message -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_attestation__accepts_one_millisecond_before_slot_start(spec, state): """Test that an attestation is accepted one millisecond before its slot starts.""" @@ -110,8 +100,18 @@ def test_gossip_beacon_attestation__accepts_one_millisecond_before_slot_start(sp subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) seen = get_seen(spec) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -119,7 +119,7 @@ def test_gossip_beacon_attestation__accepts_one_millisecond_before_slot_start(sp yield "messages", "meta", [build_message(attestation, subnet_id, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_attestation__accepts_at_slot_start(spec, state): """Test that an attestation is accepted exactly at its slot start.""" @@ -136,8 +136,18 @@ def test_gossip_beacon_attestation__accepts_at_slot_start(spec, state): subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) seen = get_seen(spec) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -145,7 +155,7 @@ def test_gossip_beacon_attestation__accepts_at_slot_start(spec, state): yield "messages", "meta", [build_message(attestation, subnet_id, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_attestation__ignores_first_slot_before_epoch_window_opens(spec, state): """ @@ -168,8 +178,18 @@ def test_gossip_beacon_attestation__ignores_first_slot_before_epoch_window_opens subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) seen = get_seen(spec) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "ignore" assert reason == "attestation slot is from a future slot" @@ -181,7 +201,7 @@ def test_gossip_beacon_attestation__ignores_first_slot_before_epoch_window_opens ) -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_attestation__accepts_first_slot_when_epoch_window_opens(spec, state): """Test that a first-slot attestation is accepted when the Deneb epoch window opens.""" @@ -201,8 +221,18 @@ def test_gossip_beacon_attestation__accepts_first_slot_when_epoch_window_opens(s subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) seen = get_seen(spec) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -210,7 +240,7 @@ def test_gossip_beacon_attestation__accepts_first_slot_when_epoch_window_opens(s yield "messages", "meta", [build_message(attestation, subnet_id, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_attestation__accepts_first_slot_when_epoch_window_closes(spec, state): """Test that a first-slot attestation is accepted at the last valid Deneb epoch time.""" @@ -230,8 +260,18 @@ def test_gossip_beacon_attestation__accepts_first_slot_when_epoch_window_closes( subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) seen = get_seen(spec) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -239,7 +279,7 @@ def test_gossip_beacon_attestation__accepts_first_slot_when_epoch_window_closes( yield "messages", "meta", [build_message(attestation, subnet_id, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_attestation__ignores_first_slot_after_epoch_window_closes(spec, state): """Test that a first-slot attestation is ignored after the Deneb epoch window closes.""" @@ -259,8 +299,18 @@ def test_gossip_beacon_attestation__ignores_first_slot_after_epoch_window_closes subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) seen = get_seen(spec) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "ignore" assert reason == "attestation epoch is not previous or current epoch" @@ -272,7 +322,7 @@ def test_gossip_beacon_attestation__ignores_first_slot_after_epoch_window_closes ) -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_attestation__accepts_last_slot_one_millisecond_before_slot_start( spec, state @@ -298,8 +348,18 @@ def test_gossip_beacon_attestation__accepts_last_slot_one_millisecond_before_slo subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) seen = get_seen(spec) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -307,7 +367,7 @@ def test_gossip_beacon_attestation__accepts_last_slot_one_millisecond_before_slo yield "messages", "meta", [build_message(attestation, subnet_id, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_attestation__accepts_last_slot_at_slot_start(spec, state): """Test that a last-slot attestation is accepted exactly at its slot start.""" @@ -328,8 +388,18 @@ def test_gossip_beacon_attestation__accepts_last_slot_at_slot_start(spec, state) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) seen = get_seen(spec) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -337,7 +407,7 @@ def test_gossip_beacon_attestation__accepts_last_slot_at_slot_start(spec, state) yield "messages", "meta", [build_message(attestation, subnet_id, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_attestation__accepts_last_slot_when_epoch_window_closes(spec, state): """Test that a last-slot attestation is accepted at the last valid Deneb epoch time.""" @@ -358,8 +428,18 @@ def test_gossip_beacon_attestation__accepts_last_slot_when_epoch_window_closes(s subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) seen = get_seen(spec) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -367,7 +447,7 @@ def test_gossip_beacon_attestation__accepts_last_slot_when_epoch_window_closes(s yield "messages", "meta", [build_message(attestation, subnet_id, current_time_ms, 0, "valid")] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_attestation__ignores_last_slot_after_epoch_window_closes(spec, state): """Test that a last-slot attestation is ignored after the Deneb epoch window closes.""" @@ -388,8 +468,18 @@ def test_gossip_beacon_attestation__ignores_last_slot_after_epoch_window_closes( subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) seen = get_seen(spec) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "ignore" assert reason == "attestation epoch is not previous or current epoch" diff --git a/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_block.py b/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_block.py index f248050a25..5cb97454d8 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_block.py +++ b/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_beacon_block.py @@ -2,21 +2,21 @@ from eth_consensus_specs.test.context import ( spec_state_test, - with_phases, + with_deneb_and_later, ) from eth_consensus_specs.test.helpers.blob import get_block_with_blob, get_max_blob_count from eth_consensus_specs.test.helpers.block import sign_block -from eth_consensus_specs.test.helpers.constants import DENEB, ELECTRA, FULU from eth_consensus_specs.test.helpers.execution_payload import ( build_state_with_complete_transition, ) from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) +from eth_consensus_specs.test.helpers.forks import is_post_gloas from eth_consensus_specs.test.helpers.gossip import ( get_filename, get_seen, - run_validate_beacon_block_gossip, + run_validate_gossip, wrap_genesis_block, ) from eth_consensus_specs.test.helpers.state import ( @@ -24,7 +24,7 @@ ) -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_block__valid_with_blob_kzg_commitments(spec, state): """ @@ -51,8 +51,17 @@ def test_gossip_beacon_block__valid_with_blob_kzg_commitments(spec, state): block_time_ms = spec.compute_time_at_slot_ms(state, signed_block.message.slot) yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = {} + if not is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "valid" assert reason is None @@ -64,7 +73,7 @@ def test_gossip_beacon_block__valid_with_blob_kzg_commitments(spec, state): ) -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_beacon_block__reject_too_many_kzg_commitments(spec, state): """ @@ -93,8 +102,17 @@ def test_gossip_beacon_block__reject_too_many_kzg_commitments(spec, state): block_time_ms = spec.compute_time_at_slot_ms(state, block.slot) yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = {} + if not is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "too many blob kzg commitments" diff --git a/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_blob_sidecar.py b/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_blob_sidecar.py index c09656425b..36d8186cab 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_blob_sidecar.py +++ b/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_blob_sidecar.py @@ -3,18 +3,23 @@ from eth_consensus_specs.test.context import ( always_bls, spec_state_test, - with_phases, + with_all_phases_from_to, ) from eth_consensus_specs.test.helpers.blob import get_block_with_blob, get_max_blob_count from eth_consensus_specs.test.helpers.block import build_empty_block_for_next_slot -from eth_consensus_specs.test.helpers.constants import DENEB, ELECTRA +from eth_consensus_specs.test.helpers.constants import DENEB, FULU from eth_consensus_specs.test.helpers.execution_payload import ( build_state_with_complete_transition, ) from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, wrap_genesis_block +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.test.helpers.state import ( state_transition_and_sign_block, @@ -42,20 +47,6 @@ def setup_store_with_anchor(spec, state): return store, anchor_block -def run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, current_time_ms -): - try: - spec.validate_blob_sidecar_gossip( - seen, store, state, blob_sidecar, current_time_ms, subnet_id - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - def correct_subnet(spec, blob_sidecar): return spec.compute_subnet_for_blob_sidecar(blob_sidecar.index) @@ -73,7 +64,7 @@ def resign_blob_sidecar_header(spec, state, blob_sidecar): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__valid(spec, state): """Test that a valid blob sidecar passes gossip validation.""" @@ -99,8 +90,14 @@ def test_gossip_blob_sidecar__valid(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "valid" assert reason is None @@ -119,7 +116,7 @@ def test_gossip_blob_sidecar__valid(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__reject_index_out_of_range(spec, state): """Test that a blob sidecar with index >= MAX_BLOBS_PER_BLOCK is rejected.""" @@ -146,8 +143,14 @@ def test_gossip_blob_sidecar__reject_index_out_of_range(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = spec.SubnetID(0) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "blob index out of range" @@ -167,7 +170,7 @@ def test_gossip_blob_sidecar__reject_index_out_of_range(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__reject_wrong_subnet(spec, state): """Test that a blob sidecar on the wrong subnet is rejected.""" @@ -194,8 +197,14 @@ def test_gossip_blob_sidecar__reject_wrong_subnet(spec, state): expected_subnet = correct_subnet(spec, blob_sidecar) wrong_subnet = spec.SubnetID((int(expected_subnet) + 1) % spec.config.BLOB_SIDECAR_SUBNET_COUNT) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, wrong_subnet, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=wrong_subnet, ) assert result == "reject" assert reason == "blob sidecar is for wrong subnet" @@ -215,7 +224,7 @@ def test_gossip_blob_sidecar__reject_wrong_subnet(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test @always_bls def test_gossip_blob_sidecar__reject_invalid_proposer_signature(spec, state): @@ -244,8 +253,14 @@ def test_gossip_blob_sidecar__reject_invalid_proposer_signature(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "invalid proposer signature on blob sidecar block header" @@ -265,7 +280,7 @@ def test_gossip_blob_sidecar__reject_invalid_proposer_signature(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__reject_invalid_inclusion_proof(spec, state): """Test that a blob sidecar with a broken inclusion proof is rejected.""" @@ -295,8 +310,14 @@ def test_gossip_blob_sidecar__reject_invalid_inclusion_proof(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "invalid blob sidecar inclusion proof" @@ -316,7 +337,7 @@ def test_gossip_blob_sidecar__reject_invalid_inclusion_proof(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__reject_invalid_kzg_proof(spec, state): """Test that a blob sidecar with an invalid KZG proof is rejected.""" @@ -344,8 +365,14 @@ def test_gossip_blob_sidecar__reject_invalid_kzg_proof(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "invalid blob kzg proof" @@ -365,7 +392,7 @@ def test_gossip_blob_sidecar__reject_invalid_kzg_proof(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__ignore_future_slot(spec, state): """Test that a blob sidecar from a future slot is ignored.""" @@ -392,8 +419,14 @@ def test_gossip_blob_sidecar__ignore_future_slot(spec, state): yield "current_time_ms", "meta", int(current_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=current_time_ms, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "blob sidecar is from a future slot" @@ -413,7 +446,7 @@ def test_gossip_blob_sidecar__ignore_future_slot(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__valid_slot_within_clock_disparity(spec, state): """Test that a blob sidecar at the future-slot boundary is valid.""" @@ -440,8 +473,14 @@ def test_gossip_blob_sidecar__valid_slot_within_clock_disparity(spec, state): yield "current_time_ms", "meta", int(current_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=current_time_ms, + subnet_id=subnet_id, ) assert result == "valid" assert reason is None @@ -460,7 +499,7 @@ def test_gossip_blob_sidecar__valid_slot_within_clock_disparity(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__ignore_not_later_than_finalized_slot(spec, state): """Test that a blob sidecar at the latest finalized slot is ignored.""" @@ -500,8 +539,14 @@ def test_gossip_blob_sidecar__ignore_not_later_than_finalized_slot(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "blob sidecar is not from a slot greater than the latest finalized slot" @@ -521,7 +566,7 @@ def test_gossip_blob_sidecar__ignore_not_later_than_finalized_slot(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__reject_proposer_index_out_of_range(spec, state): """Test that a blob sidecar with proposer_index out of range is rejected.""" @@ -550,8 +595,14 @@ def test_gossip_blob_sidecar__reject_proposer_index_out_of_range(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "proposer index out of range" @@ -571,7 +622,7 @@ def test_gossip_blob_sidecar__reject_proposer_index_out_of_range(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__ignore_parent_not_seen(spec, state): """Test that a blob sidecar whose parent is unknown to the store is ignored.""" @@ -601,8 +652,14 @@ def test_gossip_blob_sidecar__ignore_parent_not_seen(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "blob sidecar's parent has not been seen" @@ -622,7 +679,7 @@ def test_gossip_blob_sidecar__ignore_parent_not_seen(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__reject_parent_failed_validation(spec, state): """Test that a blob sidecar whose parent failed validation is rejected.""" @@ -669,8 +726,14 @@ def test_gossip_blob_sidecar__reject_parent_failed_validation(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "blob sidecar's parent failed validation" @@ -690,9 +753,9 @@ def test_gossip_blob_sidecar__reject_parent_failed_validation(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test -def test_gossip_blob_sidecar__ignore_already_seen_tuple(spec, state): +def test_gossip_blob_sidecar__ignore_already_seen(spec, state): """ Test that a duplicate blob sidecar for the same (slot, proposer_index, index) tuple is ignored. @@ -722,8 +785,14 @@ def test_gossip_blob_sidecar__ignore_already_seen_tuple(spec, state): subnet_id = correct_subnet(spec, blob_sidecar) # First delivery passes. - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "valid" messages.append( @@ -736,8 +805,14 @@ def test_gossip_blob_sidecar__ignore_already_seen_tuple(spec, state): ) # Second delivery of the same sidecar is ignored. - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 600 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 600, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "already seen blob sidecar from this proposer for this slot and index" @@ -754,7 +829,7 @@ def test_gossip_blob_sidecar__ignore_already_seen_tuple(spec, state): yield "messages", "meta", messages -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__reject_slot_not_higher_than_parent(spec, state): """ @@ -803,8 +878,14 @@ def test_gossip_blob_sidecar__reject_slot_not_higher_than_parent(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "blob sidecar is not from a higher slot than its parent" @@ -824,7 +905,7 @@ def test_gossip_blob_sidecar__reject_slot_not_higher_than_parent(spec, state): ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__reject_non_ancestor_finalized_checkpoint(spec, state): """Test that a blob sidecar is rejected if the finalized checkpoint is not an ancestor.""" @@ -857,8 +938,14 @@ def test_gossip_blob_sidecar__reject_non_ancestor_finalized_checkpoint(spec, sta yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "finalized checkpoint is not an ancestor of blob sidecar's block" @@ -878,7 +965,7 @@ def test_gossip_blob_sidecar__reject_non_ancestor_finalized_checkpoint(spec, sta ) -@with_phases([DENEB, ELECTRA]) +@with_all_phases_from_to(DENEB, FULU) @spec_state_test def test_gossip_blob_sidecar__reject_wrong_proposer_index(spec, state): """Test that a blob sidecar with the wrong proposer_index is rejected.""" @@ -909,8 +996,14 @@ def test_gossip_blob_sidecar__reject_wrong_proposer_index(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, blob_sidecar) - result, reason = run_validate_blob_sidecar_gossip( - spec, seen, store, state, blob_sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + blob_sidecar=blob_sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "blob sidecar proposer_index does not match expected proposer" diff --git a/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_voluntary_exit.py b/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_voluntary_exit.py index a79fee726c..8ce8f3b265 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_voluntary_exit.py +++ b/tests/core/pyspec/eth_consensus_specs/test/deneb/networking/test_gossip_voluntary_exit.py @@ -1,10 +1,9 @@ from eth_consensus_specs.test.context import ( always_bls, spec_state_test, - with_phases, + with_deneb_and_later, ) -from eth_consensus_specs.test.helpers.constants import DENEB, ELECTRA, FULU -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen +from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, run_validate_gossip from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.test.helpers.voluntary_exits import ( sign_voluntary_exit, @@ -20,17 +19,7 @@ def create_signed_voluntary_exit(spec, state, validator_index, epoch=None, fork_ ) -def run_validate_voluntary_exit_gossip(spec, seen, state, signed_voluntary_exit): - try: - spec.validate_voluntary_exit_gossip(seen, state, signed_voluntary_exit) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test def test_gossip_voluntary_exit__valid_capella_signature(spec, state): """ @@ -46,14 +35,16 @@ def test_gossip_voluntary_exit__valid_capella_signature(spec, state): signed_exit = create_signed_voluntary_exit(spec, state, validator_index=0) yield get_filename(signed_exit), signed_exit - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "valid" assert reason is None yield "messages", "meta", [{"message": get_filename(signed_exit), "expected": "valid"}] -@with_phases([DENEB, ELECTRA, FULU]) +@with_deneb_and_later @spec_state_test @always_bls def test_gossip_voluntary_exit__reject_deneb_signature(spec, state): @@ -73,7 +64,9 @@ def test_gossip_voluntary_exit__reject_deneb_signature(spec, state): ) yield get_filename(signed_exit), signed_exit - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "reject" assert reason == "invalid voluntary exit signature" diff --git a/tests/core/pyspec/eth_consensus_specs/test/electra/networking/test_gossip_beacon_aggregate_and_proof.py b/tests/core/pyspec/eth_consensus_specs/test/electra/networking/test_gossip_beacon_aggregate_and_proof.py index ec0c85d736..200e5cfc14 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/electra/networking/test_gossip_beacon_aggregate_and_proof.py +++ b/tests/core/pyspec/eth_consensus_specs/test/electra/networking/test_gossip_beacon_aggregate_and_proof.py @@ -1,16 +1,23 @@ from eth_consensus_specs.test.context import ( spec_state_test, - with_phases, + with_all_phases_from_to, + with_electra_and_later, with_presets, ) from eth_consensus_specs.test.helpers.attestations import ( get_valid_attestation, ) -from eth_consensus_specs.test.helpers.constants import ELECTRA, FULU, MINIMAL +from eth_consensus_specs.test.helpers.constants import ELECTRA, GLOAS, MINIMAL from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, wrap_genesis_block +from eth_consensus_specs.test.helpers.forks import is_post_gloas +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.test.helpers.state import next_slot @@ -39,20 +46,6 @@ def create_signed_aggregate_and_proof(spec, state, attestation): return spec.SignedAggregateAndProof(message=aggregate_and_proof, signature=signature) -def run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_aggregate_and_proof, current_time_ms -): - try: - spec.validate_beacon_aggregate_and_proof_gossip( - seen, store, state, signed_aggregate_and_proof, current_time_ms - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - def prepare_signed_aggregate(spec, state): store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) signed_anchor = wrap_genesis_block(spec, anchor_block) @@ -63,7 +56,7 @@ def prepare_signed_aggregate(spec, state): return store, signed_anchor, signed_agg -@with_phases([ELECTRA, FULU]) +@with_electra_and_later @spec_state_test @with_presets([MINIMAL], "need multiple committees per slot") def test_gossip_beacon_aggregate_and_proof__accept_same_data_for_disjoint_committees(spec, state): @@ -116,15 +109,33 @@ def test_gossip_beacon_aggregate_and_proof__accept_same_data_for_disjoint_commit block_time_ms = spec.compute_time_at_slot_ms(state, attestation_1.data.slot) yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg_1, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg_1, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "valid" assert reason is None messages.append({"offset_ms": 500, "message": get_filename(signed_agg_1), "expected": "valid"}) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg_2, block_time_ms + 600 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg_2, + current_time_ms=block_time_ms + 600, + **kwargs, ) assert result == "valid" assert reason is None @@ -133,7 +144,7 @@ def test_gossip_beacon_aggregate_and_proof__accept_same_data_for_disjoint_commit yield "messages", "meta", messages -@with_phases([ELECTRA, FULU]) +@with_all_phases_from_to(ELECTRA, GLOAS) @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_nonzero_data_index(spec, state): """ @@ -156,8 +167,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_nonzero_data_index(spec, stat block_time_ms = spec.compute_time_at_slot_ms(state, signed_agg.message.aggregate.data.slot) yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "aggregate data index is non-zero" @@ -176,7 +196,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_nonzero_data_index(spec, stat ) -@with_phases([ELECTRA, FULU]) +@with_electra_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_zero_committees(spec, state): """ @@ -199,8 +219,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_zero_committees(spec, state): block_time_ms = spec.compute_time_at_slot_ms(state, signed_agg.message.aggregate.data.slot) yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "aggregate committee bits must specify exactly one committee" @@ -219,7 +248,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_zero_committees(spec, state): ) -@with_phases([ELECTRA, FULU]) +@with_electra_and_later @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_multiple_committees(spec, state): """ @@ -248,8 +277,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_multiple_committees(spec, sta block_time_ms = spec.compute_time_at_slot_ms(state, signed_agg.message.aggregate.data.slot) yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "aggregate committee bits must specify exactly one committee" diff --git a/tests/core/pyspec/eth_consensus_specs/test/electra/networking/test_gossip_beacon_attestation.py b/tests/core/pyspec/eth_consensus_specs/test/electra/networking/test_gossip_beacon_attestation.py index 76cf63b032..68c627f776 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/electra/networking/test_gossip_beacon_attestation.py +++ b/tests/core/pyspec/eth_consensus_specs/test/electra/networking/test_gossip_beacon_attestation.py @@ -1,16 +1,23 @@ from eth_consensus_specs.test.context import ( spec_state_test, - with_phases, + with_all_phases_from_to, + with_electra_and_later, ) from eth_consensus_specs.test.helpers.attestations import ( get_valid_attestation, to_single_attestation, ) -from eth_consensus_specs.test.helpers.constants import ELECTRA, FULU +from eth_consensus_specs.test.helpers.constants import ELECTRA, GLOAS from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, wrap_genesis_block +from eth_consensus_specs.test.helpers.forks import is_post_gloas +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) from eth_consensus_specs.test.helpers.state import next_slot @@ -21,20 +28,6 @@ def get_correct_subnet(spec, state, attestation): ) -def run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms -): - try: - spec.validate_beacon_attestation_gossip( - seen, store, state, attestation, current_time_ms, subnet_id - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - def prepare_single_attestation(spec, state): store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) signed_anchor = wrap_genesis_block(spec, anchor_block) @@ -45,7 +38,7 @@ def prepare_single_attestation(spec, state): return store, signed_anchor, single -@with_phases([ELECTRA, FULU]) +@with_all_phases_from_to(ELECTRA, GLOAS) @spec_state_test def test_gossip_beacon_attestation__reject_nonzero_data_index(spec, state): """ @@ -69,8 +62,18 @@ def test_gossip_beacon_attestation__reject_nonzero_data_index(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "reject" assert reason == "attestation data index is non-zero" @@ -90,7 +93,7 @@ def test_gossip_beacon_attestation__reject_nonzero_data_index(spec, state): ) -@with_phases([ELECTRA, FULU]) +@with_electra_and_later @spec_state_test def test_gossip_beacon_attestation__reject_attester_not_in_committee(spec, state): """ @@ -120,8 +123,18 @@ def test_gossip_beacon_attestation__reject_attester_not_in_committee(spec, state yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "reject" assert reason == "attester is not a member of the committee" diff --git a/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_beacon_block.py b/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_beacon_block.py index 460b3dfebb..66306bf8b6 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_beacon_block.py +++ b/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_beacon_block.py @@ -4,10 +4,10 @@ from eth_consensus_specs.test.context import ( spec_configured_state_test, - with_phases, + with_all_phases_from_to, ) from eth_consensus_specs.test.helpers.blob import get_block_with_blob, get_max_blob_count -from eth_consensus_specs.test.helpers.constants import FULU +from eth_consensus_specs.test.helpers.constants import FULU, GLOAS from eth_consensus_specs.test.helpers.execution_payload import ( build_state_with_complete_transition, ) @@ -17,7 +17,7 @@ from eth_consensus_specs.test.helpers.gossip import ( get_filename, get_seen, - run_validate_beacon_block_gossip, + run_validate_gossip, wrap_genesis_block, ) from eth_consensus_specs.test.helpers.state import ( @@ -25,7 +25,7 @@ ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_configured_state_test( { "BLOB_SCHEDULE": (frozendict({"EPOCH": 0, "MAX_BLOBS_PER_BLOCK": 12}),), @@ -63,8 +63,14 @@ def test_gossip_beacon_block__valid_at_blob_parameters_limit(spec, state): block_time_ms = spec.compute_time_at_slot_ms(state, signed_block.message.slot) yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + block_payload_statuses={}, ) assert result == "valid" assert reason is None diff --git a/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_data_column_sidecar.py b/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_data_column_sidecar.py index 199f3f13b4..031779241c 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_data_column_sidecar.py +++ b/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_data_column_sidecar.py @@ -4,24 +4,26 @@ always_bls, spec_configured_state_test, spec_state_test, - with_phases, + with_all_phases_from_to, + with_fulu_and_later, ) from eth_consensus_specs.test.helpers.blob import ( get_block_with_blob_and_sidecars, get_max_blob_count, ) from eth_consensus_specs.test.helpers.block import build_empty_block_for_next_slot -from eth_consensus_specs.test.helpers.constants import FULU +from eth_consensus_specs.test.helpers.constants import FULU, GLOAS from eth_consensus_specs.test.helpers.execution_payload import ( build_state_with_complete_transition, ) from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) +from eth_consensus_specs.test.helpers.forks import is_post_gloas from eth_consensus_specs.test.helpers.gossip import ( get_filename, get_seen, - run_validate_data_column_sidecar_gossip, + run_validate_gossip, wrap_genesis_block, ) from eth_consensus_specs.test.helpers.keys import privkeys @@ -63,7 +65,7 @@ def resign_sidecar_header(spec, state, sidecar): sidecar.signed_block_header.signature = spec.bls.Sign(privkeys[proposer_index], signing_root) -@with_phases([FULU]) +@with_fulu_and_later @spec_configured_state_test( { "BLOB_SCHEDULE": (frozendict({"EPOCH": 0, "MAX_BLOBS_PER_BLOCK": 12}),), @@ -74,31 +76,46 @@ def test_gossip_data_column_sidecar__valid(spec, state): """Test that a valid data column sidecar passes gossip validation.""" yield "topic", "meta", "data_column_sidecar" - state = build_state_with_complete_transition(spec, state) + if not is_post_gloas(spec): + state = build_state_with_complete_transition(spec, state) yield "state", state seen = get_seen(spec) store, anchor_block = setup_store_with_anchor(spec, state) signed_anchor = wrap_genesis_block(spec, anchor_block) yield get_filename(signed_anchor), signed_anchor - yield "blocks", "meta", [{"block": get_filename(signed_anchor)}] max_blobs = get_max_blob_count(spec, state) # Sanity check: the BLOB_SCHEDULE override should be exercising the Fulu # code path (`get_blob_parameters`), not the Electra fallback. A client that # forgets EIP-7892 and uses MAX_BLOBS_PER_BLOCK_ELECTRA would reject this sidecar. assert max_blobs > spec.config.MAX_BLOBS_PER_BLOCK_ELECTRA - _, sidecars = build_signed_block_and_sidecars(spec, state, blob_count=max_blobs) + signed_block, sidecars = build_signed_block_and_sidecars(spec, state, blob_count=max_blobs) sidecar = sidecars[0] + blocks_meta = [{"block": get_filename(signed_anchor)}] + if is_post_gloas(spec): + # gloas's validator requires the sidecar's referenced block to be in store. + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + yield get_filename(signed_block), signed_block + blocks_meta.append({"block": get_filename(signed_block)}) + yield "blocks", "meta", blocks_meta + yield get_filename(sidecar), sidecar - block_time_ms = spec.compute_time_at_slot_ms(state, sidecar.signed_block_header.message.slot) + sidecar_slot = sidecar.slot if is_post_gloas(spec) else sidecar.signed_block_header.message.slot + block_time_ms = spec.compute_time_at_slot_ms(state, sidecar_slot) yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + kwargs = {} + if not is_post_gloas(spec): + kwargs["state"] = state + kwargs["current_time_ms"] = block_time_ms + 500 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, sidecar=sidecar, subnet_id=subnet_id, **kwargs ) assert result == "valid" assert reason is None @@ -117,7 +134,7 @@ def test_gossip_data_column_sidecar__valid(spec, state): ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__reject_index_out_of_range(spec, state): """Test that a data column sidecar with index >= NUMBER_OF_COLUMNS is rejected.""" @@ -142,8 +159,14 @@ def test_gossip_data_column_sidecar__reject_index_out_of_range(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = spec.SubnetID(0) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "invalid sidecar" @@ -163,7 +186,7 @@ def test_gossip_data_column_sidecar__reject_index_out_of_range(spec, state): ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__reject_too_many_commitments(spec, state): """Test that a data column sidecar with too many commitments is rejected.""" @@ -192,8 +215,14 @@ def test_gossip_data_column_sidecar__reject_too_many_commitments(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "invalid sidecar" @@ -213,13 +242,14 @@ def test_gossip_data_column_sidecar__reject_too_many_commitments(spec, state): ) -@with_phases([FULU]) +@with_fulu_and_later @spec_state_test def test_gossip_data_column_sidecar__reject_wrong_subnet(spec, state): """Test that a data column sidecar on the wrong subnet is rejected.""" yield "topic", "meta", "data_column_sidecar" - state = build_state_with_complete_transition(spec, state) + if not is_post_gloas(spec): + state = build_state_with_complete_transition(spec, state) yield "state", state seen = get_seen(spec) @@ -233,15 +263,20 @@ def test_gossip_data_column_sidecar__reject_wrong_subnet(spec, state): yield get_filename(sidecar), sidecar - block_time_ms = spec.compute_time_at_slot_ms(state, sidecar.signed_block_header.message.slot) + sidecar_slot = sidecar.slot if is_post_gloas(spec) else sidecar.signed_block_header.message.slot + block_time_ms = spec.compute_time_at_slot_ms(state, sidecar_slot) yield "current_time_ms", "meta", int(block_time_ms) expected_subnet = correct_subnet(spec, sidecar) wrong_subnet = spec.SubnetID( (int(expected_subnet) + 1) % spec.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT ) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, wrong_subnet, block_time_ms + 500 + kwargs = {} + if not is_post_gloas(spec): + kwargs["state"] = state + kwargs["current_time_ms"] = block_time_ms + 500 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, sidecar=sidecar, subnet_id=wrong_subnet, **kwargs ) assert result == "reject" assert reason == "sidecar is for wrong subnet" @@ -261,7 +296,7 @@ def test_gossip_data_column_sidecar__reject_wrong_subnet(spec, state): ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__ignore_future_slot(spec, state): """Test that a data column sidecar from a future slot is ignored.""" @@ -286,8 +321,14 @@ def test_gossip_data_column_sidecar__ignore_future_slot(spec, state): yield "current_time_ms", "meta", int(current_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=current_time_ms, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "sidecar is from a future slot" @@ -307,7 +348,7 @@ def test_gossip_data_column_sidecar__ignore_future_slot(spec, state): ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__valid_slot_within_clock_disparity(spec, state): """Test that a data column sidecar at the future-slot boundary is valid.""" @@ -332,8 +373,14 @@ def test_gossip_data_column_sidecar__valid_slot_within_clock_disparity(spec, sta yield "current_time_ms", "meta", int(current_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=current_time_ms, + subnet_id=subnet_id, ) assert result == "valid" assert reason is None @@ -352,7 +399,7 @@ def test_gossip_data_column_sidecar__valid_slot_within_clock_disparity(spec, sta ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__ignore_not_later_than_finalized_slot(spec, state): """Test that a data column sidecar at the latest finalized slot is ignored.""" @@ -392,8 +439,14 @@ def test_gossip_data_column_sidecar__ignore_not_later_than_finalized_slot(spec, yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "sidecar is not from a slot greater than the latest finalized slot" @@ -413,7 +466,7 @@ def test_gossip_data_column_sidecar__ignore_not_later_than_finalized_slot(spec, ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__reject_proposer_index_out_of_range(spec, state): """Test that a data column sidecar with proposer_index out of range is rejected.""" @@ -438,8 +491,14 @@ def test_gossip_data_column_sidecar__reject_proposer_index_out_of_range(spec, st yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "proposer index out of range" @@ -459,7 +518,7 @@ def test_gossip_data_column_sidecar__reject_proposer_index_out_of_range(spec, st ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test @always_bls def test_gossip_data_column_sidecar__reject_invalid_proposer_signature(spec, state): @@ -485,8 +544,14 @@ def test_gossip_data_column_sidecar__reject_invalid_proposer_signature(spec, sta yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "invalid proposer signature on sidecar block header" @@ -506,7 +571,7 @@ def test_gossip_data_column_sidecar__reject_invalid_proposer_signature(spec, sta ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__ignore_parent_not_seen(spec, state): """Test that a data column sidecar whose parent is unknown to the store is ignored.""" @@ -534,8 +599,14 @@ def test_gossip_data_column_sidecar__ignore_parent_not_seen(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "sidecar's parent has not been seen" @@ -555,7 +626,7 @@ def test_gossip_data_column_sidecar__ignore_parent_not_seen(spec, state): ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__reject_parent_failed_validation(spec, state): """Test that a data column sidecar whose parent failed validation is rejected.""" @@ -600,8 +671,14 @@ def test_gossip_data_column_sidecar__reject_parent_failed_validation(spec, state yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "sidecar's parent failed validation" @@ -621,7 +698,7 @@ def test_gossip_data_column_sidecar__reject_parent_failed_validation(spec, state ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__reject_slot_not_higher_than_parent(spec, state): """ @@ -666,8 +743,14 @@ def test_gossip_data_column_sidecar__reject_slot_not_higher_than_parent(spec, st yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "sidecar is not from a higher slot than its parent" @@ -687,7 +770,7 @@ def test_gossip_data_column_sidecar__reject_slot_not_higher_than_parent(spec, st ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__reject_non_ancestor_finalized_checkpoint(spec, state): """Test that a data column sidecar is rejected if the finalized checkpoint is not an ancestor.""" @@ -718,8 +801,14 @@ def test_gossip_data_column_sidecar__reject_non_ancestor_finalized_checkpoint(sp yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "finalized checkpoint is not an ancestor of sidecar's block" @@ -739,7 +828,7 @@ def test_gossip_data_column_sidecar__reject_non_ancestor_finalized_checkpoint(sp ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__reject_invalid_inclusion_proof(spec, state): """Test that a data column sidecar with a broken inclusion proof is rejected.""" @@ -765,8 +854,14 @@ def test_gossip_data_column_sidecar__reject_invalid_inclusion_proof(spec, state) yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "invalid sidecar inclusion proof" @@ -786,36 +881,51 @@ def test_gossip_data_column_sidecar__reject_invalid_inclusion_proof(spec, state) ) -@with_phases([FULU]) +@with_fulu_and_later @spec_state_test def test_gossip_data_column_sidecar__reject_invalid_kzg_proofs(spec, state): """Test that a data column sidecar with invalid KZG cell proofs is rejected.""" yield "topic", "meta", "data_column_sidecar" - state = build_state_with_complete_transition(spec, state) + if not is_post_gloas(spec): + state = build_state_with_complete_transition(spec, state) yield "state", state seen = get_seen(spec) store, anchor_block = setup_store_with_anchor(spec, state) signed_anchor = wrap_genesis_block(spec, anchor_block) yield get_filename(signed_anchor), signed_anchor - yield "blocks", "meta", [{"block": get_filename(signed_anchor)}] - _, sidecars = build_signed_block_and_sidecars(spec, state, blob_count=1) + signed_block, sidecars = build_signed_block_and_sidecars(spec, state, blob_count=1) sidecar = sidecars[0] # Corrupt every KZG proof to the point at infinity, which won't verify # against the real commitments. bad_proof = spec.KZGProof(b"\xc0" + b"\x00" * 47) sidecar.kzg_proofs = [bad_proof for _ in sidecar.kzg_proofs] + blocks_meta = [{"block": get_filename(signed_anchor)}] + if is_post_gloas(spec): + # gloas's validator requires the sidecar's referenced block to be in store. + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + yield get_filename(signed_block), signed_block + blocks_meta.append({"block": get_filename(signed_block)}) + yield "blocks", "meta", blocks_meta + yield get_filename(sidecar), sidecar - block_time_ms = spec.compute_time_at_slot_ms(state, sidecar.signed_block_header.message.slot) + sidecar_slot = sidecar.slot if is_post_gloas(spec) else sidecar.signed_block_header.message.slot + block_time_ms = spec.compute_time_at_slot_ms(state, sidecar_slot) yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + kwargs = {} + if not is_post_gloas(spec): + kwargs["state"] = state + kwargs["current_time_ms"] = block_time_ms + 500 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, sidecar=sidecar, subnet_id=subnet_id, **kwargs ) assert result == "reject" assert reason == "invalid sidecar kzg proofs" @@ -835,9 +945,9 @@ def test_gossip_data_column_sidecar__reject_invalid_kzg_proofs(spec, state): ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test -def test_gossip_data_column_sidecar__ignore_already_seen_tuple(spec, state): +def test_gossip_data_column_sidecar__ignore_already_seen(spec, state): """ Test that a duplicate data column sidecar for the same (slot, proposer_index, index) tuple is ignored. @@ -865,8 +975,14 @@ def test_gossip_data_column_sidecar__ignore_already_seen_tuple(spec, state): subnet_id = correct_subnet(spec, sidecar) # First delivery passes. - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "valid" messages.append( @@ -879,8 +995,14 @@ def test_gossip_data_column_sidecar__ignore_already_seen_tuple(spec, state): ) # Second delivery of the same sidecar is ignored. - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 600 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 600, + subnet_id=subnet_id, ) assert result == "ignore" assert reason == "already seen sidecar from this proposer for this slot and index" @@ -897,7 +1019,7 @@ def test_gossip_data_column_sidecar__ignore_already_seen_tuple(spec, state): yield "messages", "meta", messages -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_data_column_sidecar__reject_wrong_proposer_index(spec, state): """Test that a data column sidecar with the wrong proposer_index is rejected.""" @@ -926,8 +1048,14 @@ def test_gossip_data_column_sidecar__reject_wrong_proposer_index(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = correct_subnet(spec, sidecar) - result, reason = run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=sidecar, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, ) assert result == "reject" assert reason == "sidecar proposer_index does not match expected proposer" diff --git a/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_partial_data_column_sidecar.py b/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_partial_data_column_sidecar.py index 7c0cd91698..bda0afdaf1 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_partial_data_column_sidecar.py +++ b/tests/core/pyspec/eth_consensus_specs/test/fulu/networking/test_gossip_partial_data_column_sidecar.py @@ -4,14 +4,14 @@ always_bls, spec_configured_state_test, spec_state_test, - with_phases, + with_all_phases_from_to, ) from eth_consensus_specs.test.helpers.blob import ( get_block_with_blob_and_sidecars, get_max_blob_count, ) from eth_consensus_specs.test.helpers.block import build_empty_block_for_next_slot -from eth_consensus_specs.test.helpers.constants import FULU +from eth_consensus_specs.test.helpers.constants import FULU, GLOAS from eth_consensus_specs.test.helpers.execution_payload import ( build_state_with_complete_transition, ) @@ -21,7 +21,7 @@ from eth_consensus_specs.test.helpers.gossip import ( get_filename, get_seen, - run_validate_partial_data_column_sidecar_gossip, + run_validate_gossip, wrap_genesis_block, ) from eth_consensus_specs.test.helpers.keys import privkeys @@ -94,7 +94,7 @@ def resign_header(spec, state, header): header.signed_block_header.signature = spec.bls.Sign(privkeys[proposer_index], signing_root) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__valid_header_only(spec, state): """Test that a header-only partial sidecar passes gossip validation.""" @@ -120,8 +120,15 @@ def test_gossip_partial_data_column_sidecar__valid_header_only(spec, state): yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "valid" assert reason is None @@ -141,7 +148,7 @@ def test_gossip_partial_data_column_sidecar__valid_header_only(spec, state): ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_configured_state_test( { "BLOB_SCHEDULE": (frozendict({"EPOCH": 0, "MAX_BLOBS_PER_BLOCK": 12}),), @@ -177,8 +184,15 @@ def test_gossip_partial_data_column_sidecar__valid_header_and_cells(spec, state) yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "valid" assert reason is None @@ -198,7 +212,7 @@ def test_gossip_partial_data_column_sidecar__valid_header_and_cells(spec, state) ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__valid_cells_only_with_cached_header(spec, state): """Test that a cells-only partial sidecar passes when a header was cached previously.""" @@ -231,8 +245,15 @@ def test_gossip_partial_data_column_sidecar__valid_cells_only_with_cached_header column_index = sidecar.index messages = [] - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, header_msg, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=header_msg, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "valid" messages.append( @@ -245,8 +266,15 @@ def test_gossip_partial_data_column_sidecar__valid_cells_only_with_cached_header } ) - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, cells_msg, block_root, column_index, block_time_ms + 600 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=cells_msg, + current_time_ms=block_time_ms + 600, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "valid" assert reason is None @@ -263,7 +291,7 @@ def test_gossip_partial_data_column_sidecar__valid_cells_only_with_cached_header yield "messages", "meta", messages -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_semantically_empty(spec, state): """Test that a partial sidecar with no header and no cells is rejected.""" @@ -289,8 +317,15 @@ def test_gossip_partial_data_column_sidecar__reject_semantically_empty(spec, sta yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "partial message is semantically empty" @@ -311,7 +346,7 @@ def test_gossip_partial_data_column_sidecar__reject_semantically_empty(spec, sta ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_cell_count_mismatch(spec, state): """Test that a partial sidecar with cell count != set bits is rejected.""" @@ -339,8 +374,15 @@ def test_gossip_partial_data_column_sidecar__reject_cell_count_mismatch(spec, st yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "number of cells does not match number of set bits" @@ -361,7 +403,7 @@ def test_gossip_partial_data_column_sidecar__reject_cell_count_mismatch(spec, st ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_proof_count_mismatch(spec, state): """Test that a partial sidecar with proof count != set bits is rejected.""" @@ -389,8 +431,15 @@ def test_gossip_partial_data_column_sidecar__reject_proof_count_mismatch(spec, s yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "number of proofs does not match number of set bits" @@ -411,7 +460,7 @@ def test_gossip_partial_data_column_sidecar__reject_proof_count_mismatch(spec, s ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_prior_header_differs(spec, state): """Test that a header differing from a previously cached one is rejected.""" @@ -448,8 +497,15 @@ def test_gossip_partial_data_column_sidecar__reject_prior_header_differs(spec, s column_index = sidecar.index messages = [] - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, good, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=good, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "valid" messages.append( @@ -462,8 +518,15 @@ def test_gossip_partial_data_column_sidecar__reject_prior_header_differs(spec, s } ) - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, diverging, block_root, column_index, block_time_ms + 600 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=diverging, + current_time_ms=block_time_ms + 600, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "header differs from previously validated header" @@ -481,7 +544,7 @@ def test_gossip_partial_data_column_sidecar__reject_prior_header_differs(spec, s yield "messages", "meta", messages -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_block_root_mismatch(spec, state): """Test that a header whose block root differs from the group id is rejected.""" @@ -507,11 +570,18 @@ def test_gossip_partial_data_column_sidecar__reject_block_root_mismatch(spec, st yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" - assert reason == "header's block root does not match partial message group id" + assert reason == "header's block root does not match group id's block root" yield ( "messages", @@ -529,7 +599,7 @@ def test_gossip_partial_data_column_sidecar__reject_block_root_mismatch(spec, st ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_empty_commitments(spec, state): """Test that a header with empty kzg_commitments is rejected.""" @@ -556,8 +626,15 @@ def test_gossip_partial_data_column_sidecar__reject_empty_commitments(spec, stat yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "header's kzg_commitments is empty" @@ -578,7 +655,7 @@ def test_gossip_partial_data_column_sidecar__reject_empty_commitments(spec, stat ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__ignore_future_slot(spec, state): """Test that a header from a future slot is ignored.""" @@ -605,8 +682,15 @@ def test_gossip_partial_data_column_sidecar__ignore_future_slot(spec, state): yield "current_time_ms", "meta", int(current_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=current_time_ms, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "ignore" assert reason == "header is from a future slot" @@ -627,7 +711,7 @@ def test_gossip_partial_data_column_sidecar__ignore_future_slot(spec, state): ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__ignore_not_later_than_finalized_slot(spec, state): """Test that a header at the latest finalized slot is ignored.""" @@ -666,8 +750,15 @@ def test_gossip_partial_data_column_sidecar__ignore_not_later_than_finalized_slo yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "ignore" assert reason == "header is not from a slot greater than the latest finalized slot" @@ -688,7 +779,7 @@ def test_gossip_partial_data_column_sidecar__ignore_not_later_than_finalized_slo ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_proposer_index_out_of_range(spec, state): """Test that a header with proposer_index out of range is rejected.""" @@ -717,8 +808,15 @@ def test_gossip_partial_data_column_sidecar__reject_proposer_index_out_of_range( yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "proposer index out of range" @@ -739,7 +837,7 @@ def test_gossip_partial_data_column_sidecar__reject_proposer_index_out_of_range( ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test @always_bls def test_gossip_partial_data_column_sidecar__reject_invalid_proposer_signature(spec, state): @@ -767,8 +865,15 @@ def test_gossip_partial_data_column_sidecar__reject_invalid_proposer_signature(s yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "invalid proposer signature on header" @@ -789,7 +894,7 @@ def test_gossip_partial_data_column_sidecar__reject_invalid_proposer_signature(s ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__ignore_parent_not_seen(spec, state): """Test that a header whose parent is unknown to the store is ignored.""" @@ -817,8 +922,15 @@ def test_gossip_partial_data_column_sidecar__ignore_parent_not_seen(spec, state) yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "ignore" assert reason == "header's parent has not been seen" @@ -839,7 +951,7 @@ def test_gossip_partial_data_column_sidecar__ignore_parent_not_seen(spec, state) ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_parent_failed_validation(spec, state): """Test that a header whose parent failed validation is rejected.""" @@ -881,8 +993,15 @@ def test_gossip_partial_data_column_sidecar__reject_parent_failed_validation(spe yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "header's parent failed validation" @@ -903,7 +1022,7 @@ def test_gossip_partial_data_column_sidecar__reject_parent_failed_validation(spe ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_slot_not_higher_than_parent(spec, state): """Test that a header whose slot is not greater than its parent's is rejected.""" @@ -949,8 +1068,15 @@ def test_gossip_partial_data_column_sidecar__reject_slot_not_higher_than_parent( yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "header is not from a higher slot than its parent" @@ -971,7 +1097,7 @@ def test_gossip_partial_data_column_sidecar__reject_slot_not_higher_than_parent( ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_non_ancestor_finalized_checkpoint(spec, state): """Test that a header is rejected if the finalized checkpoint is not an ancestor.""" @@ -1004,8 +1130,15 @@ def test_gossip_partial_data_column_sidecar__reject_non_ancestor_finalized_check yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "finalized checkpoint is not an ancestor of header's block" @@ -1026,7 +1159,7 @@ def test_gossip_partial_data_column_sidecar__reject_non_ancestor_finalized_check ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_invalid_inclusion_proof(spec, state): """Test that a header with a broken inclusion proof is rejected.""" @@ -1055,8 +1188,15 @@ def test_gossip_partial_data_column_sidecar__reject_invalid_inclusion_proof(spec yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "invalid header inclusion proof" @@ -1077,7 +1217,7 @@ def test_gossip_partial_data_column_sidecar__reject_invalid_inclusion_proof(spec ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_wrong_proposer_index(spec, state): """Test that a header with the wrong proposer_index is rejected.""" @@ -1108,8 +1248,15 @@ def test_gossip_partial_data_column_sidecar__reject_wrong_proposer_index(spec, s yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "header proposer_index does not match expected proposer" @@ -1130,7 +1277,7 @@ def test_gossip_partial_data_column_sidecar__reject_wrong_proposer_index(spec, s ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__ignore_cells_without_cached_header(spec, state): """Test that a cells-only partial sidecar is ignored when no header is cached.""" @@ -1156,8 +1303,15 @@ def test_gossip_partial_data_column_sidecar__ignore_cells_without_cached_header( yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "ignore" assert reason == "valid corresponding header has not been seen" @@ -1178,7 +1332,7 @@ def test_gossip_partial_data_column_sidecar__ignore_cells_without_cached_header( ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__ignore_cells_with_cached_header_future_slot( spec, state @@ -1211,14 +1365,28 @@ def test_gossip_partial_data_column_sidecar__ignore_cells_with_cached_header_fut column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, header_msg, block_root, column_index, current_time_ms + 1 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=header_msg, + current_time_ms=current_time_ms + 1, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "valid" assert reason is None - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, cells_msg, block_root, column_index, current_time_ms + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=cells_msg, + current_time_ms=current_time_ms, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "ignore" assert reason == "corresponding header is from a future slot" @@ -1246,7 +1414,7 @@ def test_gossip_partial_data_column_sidecar__ignore_cells_with_cached_header_fut ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__ignore_cells_with_cached_header_not_later_than_finalized_slot( spec, state @@ -1304,8 +1472,15 @@ def test_gossip_partial_data_column_sidecar__ignore_cells_with_cached_header_not ) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, cells_msg, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=cells_msg, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "ignore" assert ( @@ -1328,7 +1503,7 @@ def test_gossip_partial_data_column_sidecar__ignore_cells_with_cached_header_not ) -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_bitmap_length_mismatch(spec, state): """ @@ -1366,8 +1541,15 @@ def test_gossip_partial_data_column_sidecar__reject_bitmap_length_mismatch(spec, column_index = sidecar.index messages = [] - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, header_msg, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=header_msg, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "valid" messages.append( @@ -1380,8 +1562,15 @@ def test_gossip_partial_data_column_sidecar__reject_bitmap_length_mismatch(spec, } ) - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, cells_msg, block_root, column_index, block_time_ms + 600 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=cells_msg, + current_time_ms=block_time_ms + 600, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "bitmap length does not match commitments length" @@ -1399,7 +1588,7 @@ def test_gossip_partial_data_column_sidecar__reject_bitmap_length_mismatch(spec, yield "messages", "meta", messages -@with_phases([FULU]) +@with_all_phases_from_to(FULU, GLOAS) @spec_state_test def test_gossip_partial_data_column_sidecar__reject_invalid_kzg_proofs(spec, state): """Test that cells with invalid KZG proofs are rejected.""" @@ -1429,8 +1618,15 @@ def test_gossip_partial_data_column_sidecar__reject_invalid_kzg_proofs(spec, sta yield "current_time_ms", "meta", int(block_time_ms) column_index = sidecar.index - result, reason = run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, partial, block_root, column_index, block_time_ms + 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + sidecar=partial, + current_time_ms=block_time_ms + 500, + group_id=spec.PartialDataColumnGroupID(block_root=block_root), + column_index=column_index, ) assert result == "reject" assert reason == "invalid sidecar kzg proofs" diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/__init__.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_beacon_aggregate_and_proof.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_beacon_aggregate_and_proof.py new file mode 100644 index 0000000000..8de23b6dbb --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_beacon_aggregate_and_proof.py @@ -0,0 +1,422 @@ +from eth_consensus_specs.test.context import ( + spec_state_test, + with_gloas_and_later, +) +from eth_consensus_specs.test.helpers.attestations import ( + get_valid_attestation, +) +from eth_consensus_specs.test.helpers.block import build_empty_block_for_next_slot +from eth_consensus_specs.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, +) +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + get_spec_block_payload_statuses, + PAYLOAD_STATUS_INVALIDATED, + PAYLOAD_STATUS_NOT_VALIDATED, + PAYLOAD_STATUS_VALID, + run_validate_gossip, + wrap_genesis_block, +) +from eth_consensus_specs.test.helpers.keys import privkeys +from eth_consensus_specs.test.helpers.state import next_slot, state_transition_and_sign_block + + +def create_signed_aggregate_and_proof(spec, state, attestation): + committee_index = spec.get_committee_indices(attestation.committee_bits)[0] + committee = spec.get_beacon_committee(state, attestation.data.slot, committee_index) + + aggregator_index = None + for index in committee: + privkey = privkeys[index] + selection_proof = spec.get_slot_signature(state, attestation.data.slot, privkey) + if spec.is_aggregator(state, attestation.data.slot, committee_index, selection_proof): + aggregator_index = index + break + + if aggregator_index is None: + aggregator_index = committee[0] + + privkey = privkeys[aggregator_index] + aggregate_and_proof = spec.get_aggregate_and_proof( + state, aggregator_index, attestation, privkey + ) + signature = spec.get_aggregate_and_proof_signature(state, aggregate_and_proof, privkey) + + return spec.SignedAggregateAndProof(message=aggregate_and_proof, signature=signature) + + +def prepare_signed_aggregate(spec, state): + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + anchor_root = anchor_block.hash_tree_root() + next_slot(spec, state) + attestation = get_valid_attestation(spec, state, signed=True, beacon_block_root=anchor_root) + signed_agg = create_signed_aggregate_and_proof(spec, state, attestation) + return store, signed_anchor, signed_agg + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_aggregate_and_proof__reject_data_index_too_high(spec, state): + """An aggregate with data.index >= 2 is rejected.""" + yield "topic", "meta", "beacon_aggregate_and_proof" + yield "state", state + + seen = get_seen(spec) + store, signed_anchor, signed_agg = prepare_signed_aggregate(spec, state) + yield get_filename(signed_anchor), signed_anchor + yield "blocks", "meta", [{"block": get_filename(signed_anchor)}] + + # Bump the data index past the gloas-allowed range. + signed_agg.message.aggregate.data.index = spec.CommitteeIndex(2) + + yield get_filename(signed_agg), signed_agg + + time_ms = spec.compute_time_at_slot_ms(state, signed_agg.message.aggregate.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=time_ms, + block_payload_statuses={}, + ) + assert result == "reject" + assert reason == "aggregate data index must be 0 or 1" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_agg), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +def prepare_same_slot_aggregate(spec, state, payload_index): + """ + Build a block at the next slot, register it in the store, then build a + signed aggregate whose data.slot matches that block's slot. + Returns (store, signed_anchor, signed_block, signed_agg, block_root). + """ + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + + attestation = get_valid_attestation( + spec, + state, + beacon_block_root=block_root, + payload_index=payload_index, + signed=True, + ) + signed_agg = create_signed_aggregate_and_proof(spec, state, attestation) + return store, signed_anchor, signed_block, signed_agg, block_root + + +def prepare_past_slot_aggregate(spec, state, payload_index): + """ + Build a block, register it, advance state so data.slot != block.slot, then + build a signed aggregate with the requested ``data.index``. + Returns (store, signed_anchor, signed_block, signed_agg, block_root). + """ + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + next_slot(spec, state) + + attestation = get_valid_attestation( + spec, + state, + beacon_block_root=block_root, + payload_index=payload_index, + signed=True, + ) + signed_agg = create_signed_aggregate_and_proof(spec, state, attestation) + return store, signed_anchor, signed_block, signed_agg, block_root + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_aggregate_and_proof__reject_same_slot_with_payload(spec, state): + """A same-slot aggregate with data.index != 0 is rejected.""" + yield "topic", "meta", "beacon_aggregate_and_proof" + yield "state", state + + store, signed_anchor, signed_block, signed_agg, _ = prepare_same_slot_aggregate( + spec, state, payload_index=1 + ) + seen = get_seen(spec) + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(signed_agg), signed_agg + + time_ms = spec.compute_time_at_slot_ms(state, signed_agg.message.aggregate.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=time_ms, + block_payload_statuses={}, + ) + assert result == "reject" + assert reason == "same-slot aggregate must attest with index 0" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_agg), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_aggregate_and_proof__ignore_payload_envelope_unseen(spec, state): + """A data.index=1 aggregate with no known payload envelope is ignored.""" + yield "topic", "meta", "beacon_aggregate_and_proof" + yield "state", state + + store, signed_anchor, signed_block, signed_agg, _ = prepare_past_slot_aggregate( + spec, state, payload_index=1 + ) + seen = get_seen(spec) + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(signed_agg), signed_agg + + time_ms = spec.compute_time_at_slot_ms(state, signed_agg.message.aggregate.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=time_ms, + block_payload_statuses={}, + ) + assert result == "ignore" + assert reason == "execution payload envelope has not been seen" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_agg), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_aggregate_and_proof__ignore_payload_pending_el_validation(spec, state): + """A data.index=1 aggregate with payload pending EL validation is ignored.""" + yield "topic", "meta", "beacon_aggregate_and_proof" + yield "state", state + + store, signed_anchor, signed_block, signed_agg, block_root = prepare_past_slot_aggregate( + spec, state, payload_index=1 + ) + seen = get_seen(spec) + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + { + "block": get_filename(signed_block), + "payload_status": PAYLOAD_STATUS_NOT_VALIDATED, + }, + ], + ) + yield get_filename(signed_agg), signed_agg + + time_ms = spec.compute_time_at_slot_ms(state, signed_agg.message.aggregate.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=time_ms, + block_payload_statuses=get_spec_block_payload_statuses( + spec, {block_root: PAYLOAD_STATUS_NOT_VALIDATED} + ), + ) + assert result == "ignore" + assert reason == "execution payload pending EL validation" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_agg), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_aggregate_and_proof__reject_payload_failed_el_validation(spec, state): + """A data.index=1 aggregate whose payload was EL-invalidated is rejected.""" + yield "topic", "meta", "beacon_aggregate_and_proof" + yield "state", state + + store, signed_anchor, signed_block, signed_agg, block_root = prepare_past_slot_aggregate( + spec, state, payload_index=1 + ) + seen = get_seen(spec) + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + { + "block": get_filename(signed_block), + "payload_status": PAYLOAD_STATUS_INVALIDATED, + }, + ], + ) + yield get_filename(signed_agg), signed_agg + + time_ms = spec.compute_time_at_slot_ms(state, signed_agg.message.aggregate.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=time_ms, + block_payload_statuses=get_spec_block_payload_statuses( + spec, {block_root: PAYLOAD_STATUS_INVALIDATED} + ), + ) + assert result == "reject" + assert reason == "execution payload failed EL validation" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_agg), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_aggregate_and_proof__valid_payload_validated(spec, state): + """A data.index=1 aggregate whose payload passed EL validation is valid.""" + yield "topic", "meta", "beacon_aggregate_and_proof" + yield "state", state + + store, signed_anchor, signed_block, signed_agg, block_root = prepare_past_slot_aggregate( + spec, state, payload_index=1 + ) + seen = get_seen(spec) + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + { + "block": get_filename(signed_block), + "payload_status": PAYLOAD_STATUS_VALID, + }, + ], + ) + yield get_filename(signed_agg), signed_agg + + time_ms = spec.compute_time_at_slot_ms(state, signed_agg.message.aggregate.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=time_ms, + block_payload_statuses=get_spec_block_payload_statuses( + spec, {block_root: PAYLOAD_STATUS_VALID} + ), + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_agg), + "expected": result, + } + ) + + yield "messages", "meta", messages diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_beacon_attestation.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_beacon_attestation.py new file mode 100644 index 0000000000..094bc21bbb --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_beacon_attestation.py @@ -0,0 +1,420 @@ +from eth_consensus_specs.test.context import ( + spec_state_test, + with_gloas_and_later, +) +from eth_consensus_specs.test.helpers.attestations import ( + get_valid_attestation, + to_single_attestation, +) +from eth_consensus_specs.test.helpers.block import build_empty_block_for_next_slot +from eth_consensus_specs.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, +) +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + get_spec_block_payload_statuses, + PAYLOAD_STATUS_INVALIDATED, + PAYLOAD_STATUS_NOT_VALIDATED, + PAYLOAD_STATUS_VALID, + run_validate_gossip, + wrap_genesis_block, +) +from eth_consensus_specs.test.helpers.state import next_slot, state_transition_and_sign_block + + +def get_correct_subnet(spec, state, attestation): + committees_per_slot = spec.get_committee_count_per_slot(state, attestation.data.target.epoch) + return spec.compute_subnet_for_attestation( + committees_per_slot, attestation.data.slot, attestation.committee_index + ) + + +def prepare_single_attestation(spec, state): + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + anchor_root = anchor_block.hash_tree_root() + next_slot(spec, state) + attestation = get_valid_attestation(spec, state, signed=False, beacon_block_root=anchor_root) + single = to_single_attestation(spec, state, attestation) + return store, signed_anchor, single + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_attestation__reject_data_index_too_high(spec, state): + """A SingleAttestation with data.index >= 2 is rejected.""" + yield "topic", "meta", "beacon_attestation" + yield "state", state + + seen = get_seen(spec) + store, signed_anchor, attestation = prepare_single_attestation(spec, state) + yield get_filename(signed_anchor), signed_anchor + yield "blocks", "meta", [{"block": get_filename(signed_anchor)}] + + attestation.data.index = spec.CommitteeIndex(2) + + yield get_filename(attestation), attestation + + time_ms = spec.compute_time_at_slot_ms(state, attestation.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + subnet_id = get_correct_subnet(spec, state, attestation) + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=time_ms, + subnet_id=subnet_id, + block_payload_statuses={}, + ) + assert result == "reject" + assert reason == "attestation data index must be 0 or 1" + messages.append( + { + "subnet_id": int(subnet_id), + "current_time_ms": int(time_ms), + "message": get_filename(attestation), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +def prepare_same_slot_attestation(spec, state, payload_index): + """ + Build a block at the next slot, register it in the store, then return a + SingleAttestation whose data.slot matches that block's slot. + Returns (store, signed_anchor, signed_block, single_attestation, subnet_id). + """ + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + + attestation = get_valid_attestation( + spec, + state, + beacon_block_root=block_root, + payload_index=payload_index, + signed=True, + ) + single = to_single_attestation(spec, state, attestation) + subnet_id = get_correct_subnet(spec, state, single) + return store, signed_anchor, signed_block, single, subnet_id + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_attestation__reject_same_slot_with_payload(spec, state): + """A same-slot attestation with data.index != 0 is rejected.""" + yield "topic", "meta", "beacon_attestation" + yield "state", state + + store, signed_anchor, signed_block, attestation, subnet_id = prepare_same_slot_attestation( + spec, state, payload_index=1 + ) + seen = get_seen(spec) + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(attestation), attestation + + time_ms = spec.compute_time_at_slot_ms(state, attestation.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=time_ms, + subnet_id=subnet_id, + block_payload_statuses={}, + ) + assert result == "reject" + assert reason == "same-slot attestation must attest with index 0" + messages.append( + { + "subnet_id": int(subnet_id), + "current_time_ms": int(time_ms), + "message": get_filename(attestation), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +def prepare_past_slot_attestation(spec, state, payload_index): + """ + Build a block at the next slot, register it, then advance state once more + so the attestation refers to a *past*-slot block (block.slot != data.slot). + Returns (store, signed_anchor, signed_block, attestation, subnet_id, block_root). + """ + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + # Advance state to a later slot so data.slot != block.slot. + next_slot(spec, state) + + attestation = get_valid_attestation( + spec, + state, + beacon_block_root=block_root, + payload_index=payload_index, + signed=True, + ) + single = to_single_attestation(spec, state, attestation) + subnet_id = get_correct_subnet(spec, state, single) + return store, signed_anchor, signed_block, single, subnet_id, block_root + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_attestation__ignore_payload_envelope_unseen(spec, state): + """A data.index=1 attestation whose payload envelope is unknown is ignored.""" + yield "topic", "meta", "beacon_attestation" + yield "state", state + + store, signed_anchor, signed_block, attestation, subnet_id, _ = prepare_past_slot_attestation( + spec, state, payload_index=1 + ) + seen = get_seen(spec) + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(attestation), attestation + + time_ms = spec.compute_time_at_slot_ms(state, attestation.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=time_ms, + subnet_id=subnet_id, + block_payload_statuses={}, + ) + assert result == "ignore" + assert reason == "execution payload envelope has not been seen" + messages.append( + { + "subnet_id": int(subnet_id), + "current_time_ms": int(time_ms), + "message": get_filename(attestation), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_attestation__ignore_payload_pending_el_validation(spec, state): + """A data.index=1 attestation whose payload is pending EL is ignored.""" + yield "topic", "meta", "beacon_attestation" + yield "state", state + + store, signed_anchor, signed_block, attestation, subnet_id, block_root = ( + prepare_past_slot_attestation(spec, state, payload_index=1) + ) + seen = get_seen(spec) + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + { + "block": get_filename(signed_block), + "payload_status": PAYLOAD_STATUS_NOT_VALIDATED, + }, + ], + ) + yield get_filename(attestation), attestation + + time_ms = spec.compute_time_at_slot_ms(state, attestation.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=time_ms, + subnet_id=subnet_id, + block_payload_statuses=get_spec_block_payload_statuses( + spec, {block_root: PAYLOAD_STATUS_NOT_VALIDATED} + ), + ) + assert result == "ignore" + assert reason == "execution payload pending EL validation" + messages.append( + { + "subnet_id": int(subnet_id), + "current_time_ms": int(time_ms), + "message": get_filename(attestation), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_attestation__reject_payload_failed_el_validation(spec, state): + """A data.index=1 attestation whose payload was EL-invalidated is rejected.""" + yield "topic", "meta", "beacon_attestation" + yield "state", state + + store, signed_anchor, signed_block, attestation, subnet_id, block_root = ( + prepare_past_slot_attestation(spec, state, payload_index=1) + ) + seen = get_seen(spec) + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + { + "block": get_filename(signed_block), + "payload_status": PAYLOAD_STATUS_INVALIDATED, + }, + ], + ) + yield get_filename(attestation), attestation + + time_ms = spec.compute_time_at_slot_ms(state, attestation.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=time_ms, + subnet_id=subnet_id, + block_payload_statuses=get_spec_block_payload_statuses( + spec, {block_root: PAYLOAD_STATUS_INVALIDATED} + ), + ) + assert result == "reject" + assert reason == "execution payload failed EL validation" + messages.append( + { + "subnet_id": int(subnet_id), + "current_time_ms": int(time_ms), + "message": get_filename(attestation), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_attestation__valid_payload_validated(spec, state): + """A data.index=1 attestation whose payload passed EL validation is valid.""" + yield "topic", "meta", "beacon_attestation" + yield "state", state + + store, signed_anchor, signed_block, attestation, subnet_id, block_root = ( + prepare_past_slot_attestation(spec, state, payload_index=1) + ) + seen = get_seen(spec) + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + { + "block": get_filename(signed_block), + "payload_status": PAYLOAD_STATUS_VALID, + }, + ], + ) + yield get_filename(attestation), attestation + + time_ms = spec.compute_time_at_slot_ms(state, attestation.data.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=time_ms, + subnet_id=subnet_id, + block_payload_statuses=get_spec_block_payload_statuses( + spec, {block_root: PAYLOAD_STATUS_VALID} + ), + ) + assert result == "valid" + assert reason is None + messages.append( + { + "subnet_id": int(subnet_id), + "current_time_ms": int(time_ms), + "message": get_filename(attestation), + "expected": result, + } + ) + + yield "messages", "meta", messages diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_beacon_block.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_beacon_block.py new file mode 100644 index 0000000000..ed94e78030 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_beacon_block.py @@ -0,0 +1,345 @@ +from eth_consensus_specs.test.context import ( + spec_state_test, + with_gloas_and_later, +) +from eth_consensus_specs.test.helpers.block import ( + build_empty_block, + build_empty_block_for_next_slot, + sign_block, +) +from eth_consensus_specs.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, +) +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) +from eth_consensus_specs.test.helpers.state import state_transition_and_sign_block + + +def setup_store_with_anchor_and_parent(spec, state): + """ + Build the genesis store, apply one block, and return + (store, signed_anchor, signed_parent, state). + """ + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + parent_block = build_empty_block_for_next_slot(spec, state) + signed_parent = state_transition_and_sign_block(spec, state, parent_block) + parent_root = signed_parent.message.hash_tree_root() + store.blocks[parent_root] = signed_parent.message + store.block_states[parent_root] = state.copy() + return store, signed_anchor, signed_parent + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_block__reject_bid_parent_root_mismatch(spec, state): + """A block whose bid parent_block_root does not match its parent_root is rejected.""" + yield "topic", "meta", "beacon_block" + + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield "blocks", "meta", [{"block": get_filename(signed_anchor)}] + + seen = get_seen(spec) + block = build_empty_block_for_next_slot(spec, state) + # Corrupt the bid's parent_block_root. + block.body.signed_execution_payload_bid.message.parent_block_root = spec.Root(b"\xab" * 32) + signed_block = sign_block(spec, state, block, proposer_index=block.proposer_index) + yield get_filename(signed_block), signed_block + + time_ms = spec.compute_time_at_slot_ms(state, signed_block.message.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "bid's parent does not equal block's parent" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_block), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_block__reject_slot_not_higher_than_parent(spec, state): + """A block whose slot is not strictly greater than its parent's slot is rejected.""" + yield "topic", "meta", "beacon_block" + + store, signed_anchor, signed_parent = setup_store_with_anchor_and_parent(spec, state) + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_parent), signed_parent + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_parent)}, + ], + ) + + # Build a "child" block claiming the parent but at the same slot. + child = build_empty_block(spec, state, slot=signed_parent.message.slot) + signed_child = sign_block(spec, state, child, proposer_index=child.proposer_index) + yield get_filename(signed_child), signed_child + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, child.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_child, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "block is not from a higher slot than its parent" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_child), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_block__reject_finalized_checkpoint_not_ancestor(spec, state): + """A block whose ancestry does not include the finalized checkpoint is rejected.""" + yield "topic", "meta", "beacon_block" + + store, signed_anchor, signed_parent = setup_store_with_anchor_and_parent(spec, state) + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_parent), signed_parent + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_parent)}, + ], + ) + + # Force the finalized checkpoint to be a root that is not an ancestor of any + # block in the store. + fake_root = spec.Root(b"\xab" * 32) + store.finalized_checkpoint = spec.Checkpoint(epoch=spec.Epoch(0), root=fake_root) + yield ( + "finalized_checkpoint", + "meta", + {"epoch": 0, "root": "0x" + "ab" * 32}, + ) + + # Build a valid-looking child block. + child = build_empty_block_for_next_slot(spec, state) + signed_child = sign_block(spec, state, child, proposer_index=child.proposer_index) + yield get_filename(signed_child), signed_child + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, child.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_child, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "finalized checkpoint is not an ancestor of block" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_child), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_block__reject_too_many_blob_commitments(spec, state): + """A block whose bid carries more KZG commitments than the per-epoch limit is rejected.""" + yield "topic", "meta", "beacon_block" + + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield "blocks", "meta", [{"block": get_filename(signed_anchor)}] + + seen = get_seen(spec) + block = build_empty_block_for_next_slot(spec, state) + max_blobs = spec.get_blob_parameters(spec.get_current_epoch(state)).max_blobs_per_block + over_limit = int(max_blobs) + 1 + block.body.signed_execution_payload_bid.message.blob_kzg_commitments = spec.List[ + spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK + ](*([spec.KZGCommitment()] * over_limit)) + signed_block = sign_block(spec, state, block, proposer_index=block.proposer_index) + yield get_filename(signed_block), signed_block + + time_ms = spec.compute_time_at_slot_ms(state, signed_block.message.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "too many blob kzg commitments" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_block), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_block__ignore_parent_state_unavailable(spec, state): + """A block whose parent state is missing from the store is ignored.""" + yield "topic", "meta", "beacon_block" + + store, signed_anchor, signed_parent = setup_store_with_anchor_and_parent(spec, state) + # Drop the parent's state (but keep the parent block) so the parent-state check fires. + del store.block_states[signed_parent.message.hash_tree_root()] + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_parent), signed_parent + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_parent)}, + ], + ) + + child = build_empty_block_for_next_slot(spec, state) + signed_child = sign_block(spec, state, child, proposer_index=child.proposer_index) + yield get_filename(signed_child), signed_child + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, child.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_child, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "block's parent state is unavailable" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_child), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_beacon_block__reject_wrong_proposer(spec, state): + """A block whose proposer index is not the expected proposer for the slot is rejected.""" + yield "topic", "meta", "beacon_block" + + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield "blocks", "meta", [{"block": get_filename(signed_anchor)}] + + block = build_empty_block_for_next_slot(spec, state) + correct_proposer = block.proposer_index + wrong_proposer = spec.ValidatorIndex((int(correct_proposer) + 1) % len(state.validators)) + block.proposer_index = wrong_proposer + # Sign with the claimed (wrong) proposer's key so the signature check passes + # and we reach the proposer-mismatch check. + signed_block = sign_block(spec, state, block, proposer_index=wrong_proposer) + yield get_filename(signed_block), signed_block + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, signed_block.message.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "block proposer does not match the expected proposer" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_block), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_data_column_sidecar.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_data_column_sidecar.py new file mode 100644 index 0000000000..c90b29186e --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_data_column_sidecar.py @@ -0,0 +1,230 @@ +from eth_consensus_specs.test.context import ( + spec_state_test, + with_gloas_and_later, +) +from eth_consensus_specs.test.helpers.blob import get_block_with_blob_and_sidecars +from eth_consensus_specs.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, +) +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) + + +def setup_gloas_sidecar(spec, state): + """ + Build a signed block carrying one blob, advance the state, and return + (store, signed_anchor, signed_block, sidecar) ready for validation. + """ + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + _, _, _, signed_block, sidecars, _ = get_block_with_blob_and_sidecars(spec, state, blob_count=1) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + return store, signed_anchor, signed_block, sidecars[0] + + +@with_gloas_and_later +@spec_state_test +def test_gossip_data_column_sidecar__ignore_block_unseen(spec, state): + """A sidecar whose beacon_block_root has no corresponding block in the store is ignored.""" + yield "topic", "meta", "data_column_sidecar" + + store, signed_anchor, signed_block, sidecar = setup_gloas_sidecar(spec, state) + # Forget the block so the sidecar's beacon_block_root is unknown. + del store.blocks[signed_block.message.hash_tree_root()] + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield "blocks", "meta", [{"block": get_filename(signed_anchor)}] + yield get_filename(sidecar), sidecar + + seen = get_seen(spec) + correct_subnet = spec.compute_subnet_for_data_column_sidecar(sidecar.index) + + time_ms = spec.compute_time_at_slot_ms(state, sidecar.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + sidecar=sidecar, + subnet_id=correct_subnet, + ) + assert result == "ignore" + assert reason == "block for sidecar's beacon block root has not been seen" + messages.append( + { + "subnet_id": int(correct_subnet), + "current_time_ms": int(time_ms), + "message": get_filename(sidecar), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_data_column_sidecar__ignore_already_seen(spec, state): + """A sidecar already in seen.data_column_sidecar_tuples is ignored.""" + yield "topic", "meta", "data_column_sidecar" + + store, signed_anchor, signed_block, sidecar = setup_gloas_sidecar(spec, state) + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(sidecar), sidecar + + seen = get_seen(spec) + seen.data_column_sidecar_tuples.add((sidecar.beacon_block_root, sidecar.index)) + correct_subnet = spec.compute_subnet_for_data_column_sidecar(sidecar.index) + + time_ms = spec.compute_time_at_slot_ms(state, sidecar.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + sidecar=sidecar, + subnet_id=correct_subnet, + ) + assert result == "ignore" + assert reason == "already seen sidecar for this block root and index" + messages.append( + { + "subnet_id": int(correct_subnet), + "current_time_ms": int(time_ms), + "message": get_filename(sidecar), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_data_column_sidecar__reject_slot_mismatch(spec, state): + """A sidecar whose slot does not match the referenced block's slot is rejected.""" + yield "topic", "meta", "data_column_sidecar" + + store, signed_anchor, signed_block, sidecar = setup_gloas_sidecar(spec, state) + # Corrupt the sidecar's slot so it no longer matches the block. + sidecar.slot = spec.Slot(sidecar.slot + 1) + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(sidecar), sidecar + + seen = get_seen(spec) + correct_subnet = spec.compute_subnet_for_data_column_sidecar(sidecar.index) + + time_ms = spec.compute_time_at_slot_ms(state, sidecar.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + sidecar=sidecar, + subnet_id=correct_subnet, + ) + assert result == "reject" + assert reason == "sidecar's slot does not match block's slot" + messages.append( + { + "subnet_id": int(correct_subnet), + "current_time_ms": int(time_ms), + "message": get_filename(sidecar), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_data_column_sidecar__reject_invalid_sidecar(spec, state): + """A sidecar whose structural validation fails is rejected.""" + yield "topic", "meta", "data_column_sidecar" + + store, signed_anchor, signed_block, sidecar = setup_gloas_sidecar(spec, state) + # Pad the column with an extra cell so its length no longer matches the + # bid's blob commitments, causing verify_data_column_sidecar to fail. + sidecar.column = spec.List[spec.Cell, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( + *sidecar.column, spec.Cell() + ) + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(sidecar), sidecar + + seen = get_seen(spec) + correct_subnet = spec.compute_subnet_for_data_column_sidecar(sidecar.index) + + time_ms = spec.compute_time_at_slot_ms(state, sidecar.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + sidecar=sidecar, + subnet_id=correct_subnet, + ) + assert result == "reject" + assert reason == "invalid sidecar" + messages.append( + { + "subnet_id": int(correct_subnet), + "current_time_ms": int(time_ms), + "message": get_filename(sidecar), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_execution_payload_bid.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_execution_payload_bid.py new file mode 100644 index 0000000000..06076468c7 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_execution_payload_bid.py @@ -0,0 +1,2029 @@ +from eth_consensus_specs.test.context import ( + spec_state_test, + with_gloas_and_later, +) +from eth_consensus_specs.test.helpers.block import build_empty_block_for_next_slot +from eth_consensus_specs.test.helpers.execution_payload import ( + build_signed_execution_payload_envelope, +) +from eth_consensus_specs.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, +) +from eth_consensus_specs.test.helpers.gloas.proposer_preferences import ( + build_signed_proposer_preferences, + find_upcoming_proposal_slot, +) +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) +from eth_consensus_specs.test.helpers.keys import builder_privkeys +from eth_consensus_specs.test.helpers.state import ( + state_transition_and_sign_block, +) + + +def setup_store_with_block(spec, state): + """Build the genesis store and apply one block. Returns (store, blocks, parent_block_root).""" + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + return store, [signed_anchor, signed_block], block_root + + +def activate_builders(spec, state): + """ + Make every builder active by ensuring deposit_epoch < finalized_checkpoint.epoch. + Genesis sets both to 0, so bumping the finalized epoch by one activates them. + """ + state.finalized_checkpoint.epoch = spec.Epoch(1) + + +def setup_store_advanced_for_bid(spec, state): + """ + Advance ``state`` to at least the (MIN_SEED_LOOKAHEAD + 1)-th epoch so + bid validation's dependent_root lookup doesn't underflow, then build a + genesis store containing every produced block and its state. + Returns (store, blocks, parent_block_root). + """ + return _build_store_advanced_to( + spec, + state, + spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)), + ) + + +def setup_store_advanced_to_epoch_end(spec, state): + """ + Like ``setup_store_advanced_for_bid``, but stop at the last slot of an epoch + so that a bid for the next slot (the first slot of the following epoch) + forces the validation function to advance the state across an epoch + boundary. Returns (store, blocks, parent_block_root). + """ + target_slot = spec.Slot( + spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 2)) - 1 + ) + return _build_store_advanced_to(spec, state, target_slot) + + +def _build_store_advanced_to(spec, state, target_slot): + """Build a genesis store and advance ``state`` to ``target_slot`` with empty + blocks, recording every produced block and its post-state in the store.""" + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + blocks = [signed_anchor] + while state.slot < target_slot: + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + blocks.append(signed_block) + return store, blocks, blocks[-1].message.hash_tree_root() + + +def build_signed_bid( + spec, + state, + builder_index, + slot, + parent_block_hash, + parent_block_root, + fee_recipient=None, + gas_limit=None, + value=None, + execution_payment=None, + valid_signature=True, +): + """Construct a SignedExecutionPayloadBid.""" + bid = spec.ExecutionPayloadBid( + parent_block_hash=parent_block_hash, + parent_block_root=parent_block_root, + block_hash=spec.Hash32(b"\x02" + b"\x00" * 31), + prev_randao=spec.get_randao_mix(state, spec.get_current_epoch(state)), + fee_recipient=fee_recipient + if fee_recipient is not None + else spec.ExecutionAddress(b"\x11" * 20), + gas_limit=gas_limit if gas_limit is not None else spec.uint64(30_000_000), + builder_index=builder_index, + slot=slot, + value=value if value is not None else spec.Gwei(0), + execution_payment=execution_payment if execution_payment is not None else spec.Gwei(0), + blob_kzg_commitments=spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK](), + execution_requests_root=spec.hash_tree_root(spec.ExecutionRequests()), + ) + if valid_signature and builder_index < len(builder_privkeys): + privkey = builder_privkeys[builder_index] + signature = spec.get_execution_payload_bid_signature(state, bid, privkey) + else: + signature = spec.BLSSignature() + return spec.SignedExecutionPayloadBid(message=bid, signature=signature) + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__valid(spec, state): + """A bid for the next slot from an active builder with matching preferences is valid.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_advanced_for_bid(spec, state) + activate_builders(spec, state) + parent_signed_block = blocks[-1] + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + common_fee = spec.ExecutionAddress(b"\x11" * 20) + # Choose a target_gas_limit equal to the parent's payload gas_limit so the + # is_gas_limit_target_compatible check accepts bid.gas_limit == target. + parent_gas_limit = state.latest_execution_payload_bid.gas_limit + time_ms += 50 + proposal_slot, validator_index = find_upcoming_proposal_slot(spec, state) + signed_prefs = build_signed_proposer_preferences( + spec, + state, + proposal_slot=proposal_slot, + validator_index=validator_index, + fee_recipient=common_fee, + target_gas_limit=parent_gas_limit, + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + } + ) + + time_ms += 10 + parent_block_root = parent_signed_block.message.hash_tree_root() + signed_envelope = build_signed_execution_payload_envelope( + spec, state, parent_block_root, parent_signed_block + ) + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + } + ) + yield get_filename(signed_prefs), signed_prefs + yield get_filename(signed_envelope), signed_envelope + + signed_bid = build_signed_bid( + spec, + state, + builder_index=spec.BuilderIndex(0), + slot=proposal_slot, + parent_block_hash=signed_envelope.message.payload.block_hash, + parent_block_root=parent_root, + fee_recipient=common_fee, + gas_limit=parent_gas_limit, + value=spec.Gwei(1), + ) + yield get_filename(signed_bid), signed_bid + + time_ms += 40 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_slot_too_far_future(spec, state): + """A bid whose slot is far in the future is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + far_slot = spec.Slot(state.slot + 100) + signed_bid = build_signed_bid( + spec, + state, + builder_index=spec.BuilderIndex(0), + slot=far_slot, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "bid slot is not the current or next slot" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_slot_outside_lower_disparity(spec, state): + """A bid whose slot is 1ms before the lower clock-disparity edge is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + bid_slot = spec.Slot(state.slot + 1) + signed_bid = build_signed_bid( + spec, + state, + builder_index=spec.BuilderIndex(0), + slot=bid_slot, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + # Lower edge: state.slot's start - MAXIMUM_GOSSIP_CLOCK_DISPARITY. One ms + # before that places the bid outside the disparity window. + time_ms = ( + spec.compute_time_at_slot_ms(state, spec.Slot(bid_slot - 1)) + - spec.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY + - 1 + ) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "bid slot is not the current or next slot" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__valid_slot_at_lower_disparity(spec, state): + """A bid whose slot lands exactly on the lower clock-disparity edge is valid.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_advanced_for_bid(spec, state) + activate_builders(spec, state) + parent_signed_block = blocks[-1] + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + common_fee = spec.ExecutionAddress(b"\x11" * 20) + parent_gas_limit = state.latest_execution_payload_bid.gas_limit + bid_slot = spec.Slot(state.slot + 1) + # Lower edge of the disparity window: (bid_slot - 1)'s start minus disparity. + time_ms = ( + spec.compute_time_at_slot_ms(state, spec.Slot(bid_slot - 1)) + - spec.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY + ) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + proposal_slot, validator_index = find_upcoming_proposal_slot(spec, state) + signed_prefs = build_signed_proposer_preferences( + spec, + state, + proposal_slot=proposal_slot, + validator_index=validator_index, + fee_recipient=common_fee, + target_gas_limit=parent_gas_limit, + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + yield get_filename(signed_prefs), signed_prefs + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + } + ) + + time_ms += 10 + parent_block_root = parent_signed_block.message.hash_tree_root() + signed_envelope = build_signed_execution_payload_envelope( + spec, state, parent_block_root, parent_signed_block + ) + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "valid" + assert reason is None + yield get_filename(signed_envelope), signed_envelope + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + } + ) + + signed_bid = build_signed_bid( + spec, + state, + builder_index=spec.BuilderIndex(0), + slot=proposal_slot, + parent_block_hash=signed_envelope.message.payload.block_hash, + parent_block_root=parent_root, + fee_recipient=common_fee, + gas_limit=parent_gas_limit, + value=spec.Gwei(1), + ) + yield get_filename(signed_bid), signed_bid + + # Validate the bid exactly at the lower edge of the disparity window. + time_ms = ( + spec.compute_time_at_slot_ms(state, spec.Slot(proposal_slot - 1)) + - spec.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__valid_slot_at_upper_disparity(spec, state): + """A bid whose slot lands exactly on the upper clock-disparity edge is valid.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_advanced_for_bid(spec, state) + activate_builders(spec, state) + parent_signed_block = blocks[-1] + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + common_fee = spec.ExecutionAddress(b"\x11" * 20) + parent_gas_limit = state.latest_execution_payload_bid.gas_limit + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + proposal_slot, validator_index = find_upcoming_proposal_slot(spec, state) + signed_prefs = build_signed_proposer_preferences( + spec, + state, + proposal_slot=proposal_slot, + validator_index=validator_index, + fee_recipient=common_fee, + target_gas_limit=parent_gas_limit, + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + yield get_filename(signed_prefs), signed_prefs + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + } + ) + + time_ms += 10 + parent_block_root = parent_signed_block.message.hash_tree_root() + signed_envelope = build_signed_execution_payload_envelope( + spec, state, parent_block_root, parent_signed_block + ) + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "valid" + assert reason is None + yield get_filename(signed_envelope), signed_envelope + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + } + ) + + signed_bid = build_signed_bid( + spec, + state, + builder_index=spec.BuilderIndex(0), + slot=proposal_slot, + parent_block_hash=signed_envelope.message.payload.block_hash, + parent_block_root=parent_root, + fee_recipient=common_fee, + gas_limit=parent_gas_limit, + value=spec.Gwei(1), + ) + yield get_filename(signed_bid), signed_bid + + # Validate the bid exactly at the upper edge of the disparity window. + time_ms = ( + spec.compute_time_at_slot_ms(state, spec.Slot(proposal_slot + 1)) + + spec.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_slot_outside_upper_disparity(spec, state): + """A bid whose slot is 1ms past the upper clock-disparity edge is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + bid_slot = spec.Slot(state.slot + 1) + signed_bid = build_signed_bid( + spec, + state, + builder_index=spec.BuilderIndex(0), + slot=bid_slot, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + # Upper edge: (bid_slot + 1)'s start + MAXIMUM_GOSSIP_CLOCK_DISPARITY. One + # ms past that places the bid outside the disparity window. + time_ms = ( + spec.compute_time_at_slot_ms(state, spec.Slot(bid_slot + 1)) + + spec.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY + + 1 + ) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "bid slot is not the current or next slot" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_duplicate_from_builder(spec, state): + """A second bid from the same builder for the same slot is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + next_slot_value = spec.Slot(state.slot + 1) + builder_index = spec.BuilderIndex(0) + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=next_slot_value, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + valid_signature=False, + ) + # Prepopulate seen so the bid is treated as a duplicate. + seen.execution_payload_bids.add((builder_index, next_slot_value)) + + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "already seen valid bid from this builder for this slot" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_not_highest_value(spec, state): + """A bid whose value does not exceed the best bid for this slot/parent is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + next_slot_value = spec.Slot(state.slot + 1) + builder_index = spec.BuilderIndex(0) + parent_hash = state.latest_block_hash + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=next_slot_value, + parent_block_hash=parent_hash, + parent_block_root=parent_root, + value=spec.Gwei(10), + valid_signature=False, + ) + # Prepopulate the best-bid cache with a higher value. + seen.best_execution_payload_bid[(next_slot_value, parent_hash, parent_root)] = spec.Gwei(100) + + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "bid is not the highest value bid seen for this slot and parent" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__reject_builder_index_out_of_range(spec, state): + """A bid whose builder_index is past the builder registry is rejected.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + next_slot_value = spec.Slot(state.slot + 1) + out_of_range_index = spec.BuilderIndex(len(state.builders)) + signed_bid = build_signed_bid( + spec, + state, + builder_index=out_of_range_index, + slot=next_slot_value, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "builder index out of range" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_builder_cannot_cover(spec, state): + """A bid whose value exceeds what the builder can cover is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + next_slot_value = spec.Slot(state.slot + 1) + builder_index = spec.BuilderIndex(0) + # Zero out the builder's balance so it cannot cover even a tiny bid. + state.builders[builder_index].balance = spec.Gwei(0) + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=next_slot_value, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "builder cannot cover bid value" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__reject_execution_payment_nonzero(spec, state): + """A bid whose execution_payment is non-zero is rejected.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + next_slot_value = spec.Slot(state.slot + 1) + builder_index = spec.BuilderIndex(0) + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=next_slot_value, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + value=spec.Gwei(1), + execution_payment=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "bid's execution payment must be zero" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__reject_builder_not_active(spec, state): + """A bid from an inactive builder is rejected.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + next_slot_value = spec.Slot(state.slot + 1) + builder_index = spec.BuilderIndex(0) + # At genesis builder.deposit_epoch (0) is not less than the finalized epoch (0), + # so is_active_builder returns False. + assert not spec.is_active_builder(state, builder_index) + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=next_slot_value, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "builder is not active" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__reject_too_many_blobs(spec, state): + """A bid whose blob KZG commitment count exceeds the per-epoch limit is rejected.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + activate_builders(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + next_slot_value = spec.Slot(state.slot + 1) + builder_index = spec.BuilderIndex(0) + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=next_slot_value, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + value=spec.Gwei(1), + valid_signature=False, + ) + # Overfill blob commitments above the per-epoch limit. + max_blobs = spec.get_blob_parameters( + spec.compute_epoch_at_slot(next_slot_value) + ).max_blobs_per_block + over_limit = int(max_blobs) + 1 + signed_bid.message.blob_kzg_commitments = spec.List[ + spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK + ](*([spec.KZGCommitment()] * over_limit)) + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "too many blob kzg commitments" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_parent_block_unknown(spec, state): + """A bid whose parent_block_root is not in store.blocks is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, _ = setup_store_with_block(spec, state) + activate_builders(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + next_slot_value = spec.Slot(state.slot + 1) + builder_index = spec.BuilderIndex(0) + unknown_root = spec.Root(b"\xab" * 32) + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=next_slot_value, + parent_block_hash=state.latest_block_hash, + parent_block_root=unknown_root, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "bid's parent block root is not a known beacon block" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__reject_slot_not_higher_than_parent(spec, state): + """A bid whose slot is not greater than its parent block's slot is rejected.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + activate_builders(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + # Parent block is at state.slot. Build a bid for state.slot (same slot) + # so bid.slot == parent.slot triggers the new REJECT. + assert blocks[-1].message.slot == state.slot + builder_index = spec.BuilderIndex(0) + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=state.slot, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "bid's slot is not higher than its parent's slot" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_parent_block_hash_unknown(spec, state): + """A bid whose parent_block_hash is not in seen.execution_payloads is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + activate_builders(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + next_slot_value = spec.Slot(state.slot + 1) + builder_index = spec.BuilderIndex(0) + unknown_hash = spec.Hash32(b"\xcd" * 32) + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=next_slot_value, + parent_block_hash=unknown_hash, + parent_block_root=parent_root, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "bid's parent block hash is not a known execution payload" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_parent_state_unavailable(spec, state): + """A bid whose parent block's state is missing from the store is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_with_block(spec, state) + activate_builders(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + # Mark the parent block's payload as known but drop its state. + seen.execution_payloads[state.latest_block_hash] = state.latest_execution_payload_bid + del store.block_states[parent_root] + + next_slot_value = spec.Slot(state.slot + 1) + builder_index = spec.BuilderIndex(0) + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=next_slot_value, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "bid's parent block state is unavailable" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_preferences_not_seen(spec, state): + """A bid whose matching proposer preferences have not been seen is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + # get_proposer_dependent_root subtracts MIN_SEED_LOOKAHEAD from the epoch, + # so the parent state must already be at least MIN_SEED_LOOKAHEAD + 1 epochs + # in for the lookup to land on a non-underflowing slot. + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + blocks = [signed_anchor] + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + while state.slot < target_slot: + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + blocks.append(signed_block) + parent_root = blocks[-1].message.hash_tree_root() + activate_builders(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + # Seed the parent payload, but leave seen.proposer_preferences empty. + seen.execution_payloads[state.latest_block_hash] = state.latest_execution_payload_bid + + next_slot_value = spec.Slot(state.slot + 1) + builder_index = spec.BuilderIndex(0) + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=next_slot_value, + parent_block_hash=state.latest_block_hash, + parent_block_root=parent_root, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "matching proposer preferences have not been seen" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__reject_fee_recipient_mismatch(spec, state): + """A bid whose fee_recipient does not match the proposer's preference is rejected.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_advanced_for_bid(spec, state) + activate_builders(spec, state) + parent_signed_block = blocks[-1] + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + bid_fee = spec.ExecutionAddress(b"\xaa" * 20) + prefs_fee = spec.ExecutionAddress(b"\xbb" * 20) + parent_gas_limit = state.latest_execution_payload_bid.gas_limit + time_ms += 50 + proposal_slot, validator_index = find_upcoming_proposal_slot(spec, state) + signed_prefs = build_signed_proposer_preferences( + spec, + state, + proposal_slot=proposal_slot, + validator_index=validator_index, + fee_recipient=prefs_fee, + target_gas_limit=parent_gas_limit, + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + } + ) + + time_ms += 10 + parent_block_root = parent_signed_block.message.hash_tree_root() + signed_envelope = build_signed_execution_payload_envelope( + spec, state, parent_block_root, parent_signed_block + ) + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + } + ) + yield get_filename(signed_prefs), signed_prefs + yield get_filename(signed_envelope), signed_envelope + + signed_bid = build_signed_bid( + spec, + state, + builder_index=spec.BuilderIndex(0), + slot=proposal_slot, + parent_block_hash=signed_envelope.message.payload.block_hash, + parent_block_root=parent_root, + fee_recipient=bid_fee, + gas_limit=parent_gas_limit, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms += 40 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "bid's fee recipient does not match the proposer's preference" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_gas_limit_incompatible(spec, state): + """A bid whose gas_limit is incompatible with the proposer's target is ignored.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_advanced_for_bid(spec, state) + activate_builders(spec, state) + parent_signed_block = blocks[-1] + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + common_fee = spec.ExecutionAddress(b"\x11" * 20) + parent_gas_limit = state.latest_execution_payload_bid.gas_limit + time_ms += 50 + proposal_slot, validator_index = find_upcoming_proposal_slot(spec, state) + signed_prefs = build_signed_proposer_preferences( + spec, + state, + proposal_slot=proposal_slot, + validator_index=validator_index, + fee_recipient=common_fee, + target_gas_limit=parent_gas_limit, + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + } + ) + + time_ms += 10 + parent_block_root = parent_signed_block.message.hash_tree_root() + signed_envelope = build_signed_execution_payload_envelope( + spec, state, parent_block_root, parent_signed_block + ) + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + } + ) + yield get_filename(signed_prefs), signed_prefs + yield get_filename(signed_envelope), signed_envelope + + # Pick a gas_limit far outside the EIP-1559 step from parent. + incompatible_gas_limit = spec.uint64(int(parent_gas_limit) + 1_000_000) + signed_bid = build_signed_bid( + spec, + state, + builder_index=spec.BuilderIndex(0), + slot=proposal_slot, + parent_block_hash=signed_envelope.message.payload.block_hash, + parent_block_root=parent_root, + fee_recipient=common_fee, + gas_limit=incompatible_gas_limit, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms += 40 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "bid gas limit is not compatible with the proposer's target" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__reject_invalid_signature(spec, state): + """A bid with an invalid signature is rejected once all other checks pass.""" + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_advanced_for_bid(spec, state) + activate_builders(spec, state) + parent_signed_block = blocks[-1] + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + common_fee = spec.ExecutionAddress(b"\x11" * 20) + parent_gas_limit = state.latest_execution_payload_bid.gas_limit + time_ms += 50 + proposal_slot, validator_index = find_upcoming_proposal_slot(spec, state) + signed_prefs = build_signed_proposer_preferences( + spec, + state, + proposal_slot=proposal_slot, + validator_index=validator_index, + fee_recipient=common_fee, + target_gas_limit=parent_gas_limit, + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + } + ) + + time_ms += 10 + parent_block_root = parent_signed_block.message.hash_tree_root() + signed_envelope = build_signed_execution_payload_envelope( + spec, state, parent_block_root, parent_signed_block + ) + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + } + ) + yield get_filename(signed_prefs), signed_prefs + yield get_filename(signed_envelope), signed_envelope + + signed_bid = build_signed_bid( + spec, + state, + builder_index=spec.BuilderIndex(0), + slot=proposal_slot, + parent_block_hash=signed_envelope.message.payload.block_hash, + parent_block_root=parent_root, + fee_recipient=common_fee, + gas_limit=parent_gas_limit, + value=spec.Gwei(1), + valid_signature=False, + ) + yield get_filename(signed_bid), signed_bid + + time_ms += 40 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "invalid bid signature" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__valid_requires_state_advanced_across_epoch(spec, state): + """ + Reference test for the state advancement performed during bid validation. + + The bid's parent is at the last slot of an epoch, and its builder can only + cover the bid once a sub-quorum pending payment has been cleared by + process_builder_pending_payments at the epoch transition. Because the + function advances the state to the bid's slot (the first slot of the next + epoch), the payment is dropped, can_builder_cover_bid passes, and the bid is + valid. + + If the state were NOT advanced, the pending payment would still count + against the builder and can_builder_cover_bid would fail, yielding an + "ignore" -- so this test fails if the advance is removed. + """ + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_advanced_to_epoch_end(spec, state) + activate_builders(spec, state) + parent_signed_block = blocks[-1] + + builder_index = spec.BuilderIndex(0) + bid_value = spec.Gwei(1) + # Fund the builder so that, after MIN_DEPOSIT_AMOUNT is reserved, it can + # cover the bid only once the pending payment below is cleared. + state.builders[builder_index].balance = spec.Gwei(spec.MIN_DEPOSIT_AMOUNT + bid_value) + # Add a pending payment (below quorum weight) in the previous-epoch half of + # the queue. It counts against coverage now, but is dropped (not promoted to + # a withdrawal) when process_builder_pending_payments runs at the epoch + # transition during the advance. + state.builder_pending_payments[0] = spec.BuilderPendingPayment( + weight=spec.Gwei(0), + withdrawal=spec.BuilderPendingWithdrawal( + fee_recipient=spec.ExecutionAddress(), + amount=bid_value, + builder_index=builder_index, + ), + ) + # Sanity check: the builder cannot cover the bid at the parent's slot, but + # can once the state is advanced across the epoch boundary. + assert not spec.can_builder_cover_bid(state, builder_index, bid_value) + advanced_state = state.copy() + spec.process_slots( + advanced_state, spec.Slot(spec.SLOTS_PER_EPOCH * (spec.MIN_SEED_LOOKAHEAD + 2)) + ) + assert spec.can_builder_cover_bid(advanced_state, builder_index, bid_value) + + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + common_fee = spec.ExecutionAddress(b"\x11" * 20) + parent_gas_limit = state.latest_execution_payload_bid.gas_limit + + # The first proposal slot after the parent is the first slot of the next + # epoch, so validating the bid must advance the state across the boundary. + time_ms += 50 + proposal_slot, validator_index = find_upcoming_proposal_slot(spec, state) + assert spec.compute_epoch_at_slot(proposal_slot) == spec.compute_epoch_at_slot(state.slot) + 1 + signed_prefs = build_signed_proposer_preferences( + spec, + state, + proposal_slot=proposal_slot, + validator_index=validator_index, + fee_recipient=common_fee, + target_gas_limit=parent_gas_limit, + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + } + ) + + time_ms += 10 + signed_envelope = build_signed_execution_payload_envelope( + spec, state, parent_root, parent_signed_block + ) + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "valid" + assert reason is None + yield get_filename(signed_prefs), signed_prefs + yield get_filename(signed_envelope), signed_envelope + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + } + ) + + signed_bid = build_signed_bid( + spec, + state, + builder_index=builder_index, + slot=proposal_slot, + parent_block_hash=signed_envelope.message.payload.block_hash, + parent_block_root=parent_root, + fee_recipient=common_fee, + gas_limit=parent_gas_limit, + value=bid_value, + ) + yield get_filename(signed_bid), signed_bid + + time_ms += 40 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + } + ) + + yield "messages", "meta", messages + + +def _run_bid_gas_limit_scenario( + spec, + state, + parent_gas_limit, + bid_gas_limit, + target_gas_limit, + expected_result, + expected_reason, +): + """ + Drive a full bid gossip validation with a specific (parent, bid, target) + gas_limit triple and assert the expected outcome. Yields the standard + bid-gossip reference fixture (state, blocks, seeded prefs + envelope, + bid, messages). + """ + yield "topic", "meta", "execution_payload_bid" + + store, blocks, parent_root = setup_store_advanced_for_bid(spec, state) + activate_builders(spec, state) + parent_signed_block = blocks[-1] + # Override the parent's bid gas_limit so the envelope's payload.gas_limit + # (which gets seeded into seen.execution_payloads) equals our target value. + state.latest_execution_payload_bid.gas_limit = spec.uint64(parent_gas_limit) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + common_fee = spec.ExecutionAddress(b"\x11" * 20) + + time_ms += 50 + proposal_slot, validator_index = find_upcoming_proposal_slot(spec, state) + signed_prefs = build_signed_proposer_preferences( + spec, + state, + proposal_slot=proposal_slot, + validator_index=validator_index, + fee_recipient=common_fee, + target_gas_limit=spec.uint64(target_gas_limit), + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + yield get_filename(signed_prefs), signed_prefs + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + } + ) + + time_ms += 10 + parent_block_root = parent_signed_block.message.hash_tree_root() + signed_envelope = build_signed_execution_payload_envelope( + spec, state, parent_block_root, parent_signed_block + ) + assert signed_envelope.message.payload.gas_limit == parent_gas_limit + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "valid" + assert reason is None + yield get_filename(signed_envelope), signed_envelope + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + } + ) + + signed_bid = build_signed_bid( + spec, + state, + builder_index=spec.BuilderIndex(0), + slot=proposal_slot, + parent_block_hash=signed_envelope.message.payload.block_hash, + parent_block_root=parent_root, + fee_recipient=common_fee, + gas_limit=spec.uint64(bid_gas_limit), + value=spec.Gwei(1), + valid_signature=(expected_result == "valid"), + ) + yield get_filename(signed_bid), signed_bid + + time_ms += 40 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_execution_payload_bid=signed_bid, + current_time_ms=time_ms, + ) + assert result == expected_result + assert reason == expected_reason + entry = { + "current_time_ms": int(time_ms), + "message": get_filename(signed_bid), + "expected": result, + } + if reason is not None: + entry["reason"] = reason + messages.append(entry) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__valid_gas_limit_increase_within_limit(spec, state): + """A bid with gas_limit raised within the EIP-1559 step toward target is valid.""" + yield from _run_bid_gas_limit_scenario( + spec, + state, + parent_gas_limit=60_000_000, + bid_gas_limit=60_000_100, + target_gas_limit=60_000_100, + expected_result="valid", + expected_reason=None, + ) + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__valid_gas_limit_increase_exceeding_limit(spec, state): + """When target is above the EIP-1559 max, a bid pinned to the max value is valid.""" + # max_gas_limit_difference = 60_000_000 // 1024 - 1 = 58_592 + yield from _run_bid_gas_limit_scenario( + spec, + state, + parent_gas_limit=60_000_000, + bid_gas_limit=60_058_592, + target_gas_limit=100_000_000, + expected_result="valid", + expected_reason=None, + ) + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_gas_limit_increase_exceeding_limit_off_by_one( + spec, state +): + """A bid one over the EIP-1559 max (when target is above the max) is ignored.""" + yield from _run_bid_gas_limit_scenario( + spec, + state, + parent_gas_limit=60_000_000, + bid_gas_limit=60_058_593, + target_gas_limit=100_000_000, + expected_result="ignore", + expected_reason="bid gas limit is not compatible with the proposer's target", + ) + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__valid_gas_limit_decrease_within_limit(spec, state): + """A bid with gas_limit lowered within the EIP-1559 step toward target is valid.""" + yield from _run_bid_gas_limit_scenario( + spec, + state, + parent_gas_limit=60_000_000, + bid_gas_limit=59_999_990, + target_gas_limit=59_999_990, + expected_result="valid", + expected_reason=None, + ) + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__valid_gas_limit_decrease_exceeding_limit(spec, state): + """When target is below the EIP-1559 min, a bid pinned to the min value is valid.""" + yield from _run_bid_gas_limit_scenario( + spec, + state, + parent_gas_limit=60_000_000, + bid_gas_limit=59_941_408, + target_gas_limit=30_000_000, + expected_result="valid", + expected_reason=None, + ) + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__ignore_gas_limit_decrease_exceeding_limit_off_by_one( + spec, state +): + """A bid one under the EIP-1559 min (when target is below the min) is ignored.""" + yield from _run_bid_gas_limit_scenario( + spec, + state, + parent_gas_limit=60_000_000, + bid_gas_limit=59_941_407, + target_gas_limit=30_000_000, + expected_result="ignore", + expected_reason="bid gas limit is not compatible with the proposer's target", + ) + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__valid_gas_limit_target_equals_parent(spec, state): + """A bid whose gas_limit equals both target and parent gas_limit is valid.""" + yield from _run_bid_gas_limit_scenario( + spec, + state, + parent_gas_limit=60_000_000, + bid_gas_limit=60_000_000, + target_gas_limit=60_000_000, + expected_result="valid", + expected_reason=None, + ) + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_bid__valid_gas_limit_parent_under_step(spec, state): + """A bid is valid when parent gas_limit is below the 1024 step (step floors to 1).""" + yield from _run_bid_gas_limit_scenario( + spec, + state, + parent_gas_limit=1023, + bid_gas_limit=1023, + target_gas_limit=60_000_000, + expected_result="valid", + expected_reason=None, + ) diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_execution_payload_envelope.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_execution_payload_envelope.py new file mode 100644 index 0000000000..b4948bf491 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_execution_payload_envelope.py @@ -0,0 +1,458 @@ +from eth_consensus_specs.test.context import ( + spec_state_test, + with_gloas_and_later, +) +from eth_consensus_specs.test.helpers.block import build_empty_block_for_next_slot +from eth_consensus_specs.test.helpers.execution_payload import ( + build_signed_execution_payload_envelope, +) +from eth_consensus_specs.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, +) +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) +from eth_consensus_specs.test.helpers.state import state_transition_and_sign_block + + +def setup_store_with_block(spec, state): + """Apply one block to the genesis store. Returns store, blocks, block_root.""" + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + return store, [signed_anchor, signed_block], signed_block, block_root + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_envelope__valid(spec, state): + """A well-formed envelope for a known block passes gossip validation.""" + yield "topic", "meta", "execution_payload" + + store, blocks, signed_block, block_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + signed_envelope = build_signed_execution_payload_envelope(spec, state, block_root, signed_block) + yield get_filename(signed_envelope), signed_envelope + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_envelope__ignore_block_unseen(spec, state): + """An envelope referencing an unknown beacon block is ignored.""" + yield "topic", "meta", "execution_payload" + + store, blocks, signed_block, block_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + signed_envelope = build_signed_execution_payload_envelope(spec, state, block_root, signed_block) + # Re-target the envelope at a block root that is not in the store. + signed_envelope.message.beacon_block_root = spec.Root(b"\xab" * 32) + yield get_filename(signed_envelope), signed_envelope + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "ignore" + assert reason == "envelope's block has not been seen" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_envelope__ignore_duplicate(spec, state): + """The second valid envelope for the same (block_root, builder) is ignored.""" + yield "topic", "meta", "execution_payload" + + store, blocks, signed_block, block_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + signed_envelope = build_signed_execution_payload_envelope(spec, state, block_root, signed_block) + yield get_filename(signed_envelope), signed_envelope + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + } + ) + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "ignore" + assert reason == "already seen envelope for this block root from this builder" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_envelope__reject_slot_mismatch(spec, state): + """An envelope whose payload.slot_number does not match block.slot is rejected.""" + yield "topic", "meta", "execution_payload" + + store, blocks, signed_block, block_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + signed_envelope = build_signed_execution_payload_envelope(spec, state, block_root, signed_block) + signed_envelope.message.payload.slot_number = spec.uint64(state.slot + 1) + yield get_filename(signed_envelope), signed_envelope + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "reject" + assert reason == "block's slot does not match payload's slot number" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_envelope__reject_block_hash_mismatch(spec, state): + """An envelope whose payload.block_hash does not match the bid is rejected.""" + yield "topic", "meta", "execution_payload" + + store, blocks, signed_block, block_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + signed_envelope = build_signed_execution_payload_envelope(spec, state, block_root, signed_block) + signed_envelope.message.payload.block_hash = spec.Hash32(b"\xcd" * 32) + yield get_filename(signed_envelope), signed_envelope + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "reject" + assert reason == "payload's block hash does not match the bid's block hash" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_envelope__reject_invalid_signature(spec, state): + """An envelope with an invalid signature is rejected.""" + yield "topic", "meta", "execution_payload" + + store, blocks, signed_block, block_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + signed_envelope = build_signed_execution_payload_envelope(spec, state, block_root, signed_block) + signed_envelope.signature = spec.BLSSignature() + yield get_filename(signed_envelope), signed_envelope + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "reject" + assert reason == "invalid envelope signature" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_envelope__ignore_pre_finalized(spec, state): + """An envelope whose payload slot is before the latest finalized slot is ignored.""" + yield "topic", "meta", "execution_payload" + + store, blocks, signed_block, block_root = setup_store_with_block(spec, state) + # Advance the finalized checkpoint past the block's slot so the envelope + # appears to be from a pre-finalized slot. + store.finalized_checkpoint = spec.Checkpoint( + epoch=spec.Epoch(spec.compute_epoch_at_slot(state.slot) + 2), + root=block_root, + ) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + yield ( + "finalized_checkpoint", + "meta", + { + "epoch": int(store.finalized_checkpoint.epoch), + "root": "0x" + block_root.hex(), + }, + ) + + seen = get_seen(spec) + signed_envelope = build_signed_execution_payload_envelope(spec, state, block_root, signed_block) + yield get_filename(signed_envelope), signed_envelope + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "ignore" + assert reason == "envelope is from a slot before the latest finalized slot" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_envelope__reject_block_failed_validation(spec, state): + """An envelope whose block is in store.blocks but not in store.block_states is rejected.""" + yield "topic", "meta", "execution_payload" + + store, blocks, signed_block, block_root = setup_store_with_block(spec, state) + # Drop the block's state so the post-validation check fires. + del store.block_states[block_root] + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + signed_envelope = build_signed_execution_payload_envelope(spec, state, block_root, signed_block) + yield get_filename(signed_envelope), signed_envelope + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "reject" + assert reason == "envelope's block failed validation" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_envelope__reject_builder_index_mismatch(spec, state): + """An envelope whose builder_index does not match the bid's builder_index is rejected.""" + yield "topic", "meta", "execution_payload" + + store, blocks, signed_block, block_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + signed_envelope = build_signed_execution_payload_envelope(spec, state, block_root, signed_block) + bid_builder_index = signed_block.message.body.signed_execution_payload_bid.message.builder_index + # Pick any builder index that differs from the bid's. We subtract 1 instead + # of adding so the value stays inside uint64 range when the bid uses the + # max-value sentinel BUILDER_INDEX_SELF_BUILD. + signed_envelope.message.builder_index = spec.BuilderIndex(int(bid_builder_index) - 1) + yield get_filename(signed_envelope), signed_envelope + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "reject" + assert reason == "envelope's builder index does not match the bid's builder index" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_execution_payload_envelope__reject_execution_requests_root_mismatch(spec, state): + """An envelope whose execution_requests root does not match the bid's is rejected.""" + yield "topic", "meta", "execution_payload" + + store, blocks, signed_block, block_root = setup_store_with_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + # Use execution_requests with a non-empty deposits list so its root differs + # from the bid's empty execution_requests_root. + non_empty_requests = spec.ExecutionRequests( + deposits=spec.List[spec.DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]( + spec.DepositRequest() + ) + ) + signed_envelope = build_signed_execution_payload_envelope( + spec, state, block_root, signed_block, execution_requests=non_empty_requests + ) + yield get_filename(signed_envelope), signed_envelope + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, seen=seen, store=store, state=state, signed_execution_payload_envelope=signed_envelope + ) + assert result == "reject" + assert reason == "envelope's execution requests root does not match the bid" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_envelope), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_payload_attestation_message.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_payload_attestation_message.py new file mode 100644 index 0000000000..a03abeb2dc --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_payload_attestation_message.py @@ -0,0 +1,515 @@ +from eth_consensus_specs.test.context import ( + spec_state_test, + with_gloas_and_later, +) +from eth_consensus_specs.test.helpers.block import build_empty_block_for_next_slot +from eth_consensus_specs.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, +) +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) +from eth_consensus_specs.test.helpers.keys import privkeys +from eth_consensus_specs.test.helpers.state import next_slot, state_transition_and_sign_block + + +def setup_store_with_one_block(spec, state): + """ + Build the genesis store, then apply one block at the next slot so that we + have a known beacon_block_root the PTC can attest to. + """ + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + return store, [signed_anchor, signed_block], block_root + + +def build_payload_attestation_message( + spec, + state, + slot, + beacon_block_root, + validator_index, + payload_present=True, + valid_signature=True, +): + """Construct a PayloadAttestationMessage signed by ``validator_index``.""" + data = spec.PayloadAttestationData( + beacon_block_root=beacon_block_root, + slot=slot, + payload_present=payload_present, + blob_data_available=True, + ) + message = spec.PayloadAttestationMessage( + validator_index=validator_index, + data=data, + signature=spec.BLSSignature(), + ) + if valid_signature: + domain = spec.get_domain(state, spec.DOMAIN_PTC_ATTESTER, spec.compute_epoch_at_slot(slot)) + signing_root = spec.compute_signing_root(data, domain) + message.signature = spec.bls.Sign(privkeys[validator_index], signing_root) + return message + + +@with_gloas_and_later +@spec_state_test +def test_gossip_payload_attestation_message__valid(spec, state): + """A PayloadAttestationMessage from a PTC member for the current slot passes.""" + yield "topic", "meta", "payload_attestation_message" + + store, blocks, block_root = setup_store_with_one_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + ptc = spec.get_ptc(state, state.slot) + validator_index = ptc[0] + message = build_payload_attestation_message( + spec, state, state.slot, block_root, validator_index + ) + yield get_filename(message), message + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + payload_attestation_message=message, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(message), + "expected": result, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_payload_attestation_message__ignore_not_current_slot(spec, state): + """A message whose slot is not the current slot is ignored.""" + yield "topic", "meta", "payload_attestation_message" + + store, blocks, block_root = setup_store_with_one_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + ptc = spec.get_ptc(state, state.slot) + validator_index = ptc[0] + message = build_payload_attestation_message( + spec, state, state.slot, block_root, validator_index + ) + yield get_filename(message), message + + # Use a current_time well past the message's slot. + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + 1000 * 1000 + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + payload_attestation_message=message, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "payload attestation message slot is not the current slot" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(message), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_payload_attestation_message__ignore_duplicate(spec, state): + """The second valid message from the same validator for the same slot is ignored.""" + yield "topic", "meta", "payload_attestation_message" + + store, blocks, block_root = setup_store_with_one_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + ptc = spec.get_ptc(state, state.slot) + validator_index = ptc[0] + message = build_payload_attestation_message( + spec, state, state.slot, block_root, validator_index + ) + yield get_filename(message), message + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + payload_attestation_message=message, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(message), + "expected": result, + } + ) + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + payload_attestation_message=message, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "already seen payload attestation message from this validator" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(message), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_payload_attestation_message__ignore_block_unseen(spec, state): + """A message attesting to an unknown beacon block is ignored.""" + yield "topic", "meta", "payload_attestation_message" + + store, blocks, _ = setup_store_with_one_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + ptc = spec.get_ptc(state, state.slot) + validator_index = ptc[0] + unknown_root = spec.Root(b"\xee" * 32) + message = build_payload_attestation_message( + spec, state, state.slot, unknown_root, validator_index + ) + yield get_filename(message), message + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + payload_attestation_message=message, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "message's block has not been seen" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(message), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_payload_attestation_message__reject_validator_not_in_ptc(spec, state): + """A message from a validator not in the PTC is rejected.""" + yield "topic", "meta", "payload_attestation_message" + + store, blocks, block_root = setup_store_with_one_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + ptc = set(spec.get_ptc(state, state.slot)) + outsider = next(spec.ValidatorIndex(i) for i in range(len(state.validators)) if i not in ptc) + message = build_payload_attestation_message(spec, state, state.slot, block_root, outsider) + yield get_filename(message), message + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + payload_attestation_message=message, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "validator is not in the payload timeliness committee" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(message), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_payload_attestation_message__reject_invalid_signature(spec, state): + """A message with an invalid signature is rejected.""" + yield "topic", "meta", "payload_attestation_message" + + store, blocks, block_root = setup_store_with_one_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + ptc = spec.get_ptc(state, state.slot) + validator_index = ptc[0] + message = build_payload_attestation_message( + spec, state, state.slot, block_root, validator_index, valid_signature=False + ) + yield get_filename(message), message + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + payload_attestation_message=message, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "invalid payload attestation message signature" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(message), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_payload_attestation_message__reject_block_failed_validation(spec, state): + """A message whose block is in store.blocks but not in store.block_states is rejected.""" + yield "topic", "meta", "payload_attestation_message" + + store, blocks, block_root = setup_store_with_one_block(spec, state) + # Drop the block's state so the post-validation check fires. + del store.block_states[block_root] + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + ptc = spec.get_ptc(state, state.slot) + validator_index = ptc[0] + message = build_payload_attestation_message( + spec, state, state.slot, block_root, validator_index + ) + yield get_filename(message), message + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + payload_attestation_message=message, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "message's block failed validation" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(message), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_payload_attestation_message__reject_validator_index_out_of_range(spec, state): + """A message whose validator index is past the validator registry is rejected.""" + yield "topic", "meta", "payload_attestation_message" + + store, blocks, block_root = setup_store_with_one_block(spec, state) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + out_of_range_index = spec.ValidatorIndex(len(state.validators)) + # Build the message by hand because build_payload_attestation_message indexes + # into privkeys for signing, which would fail for an out-of-range index. + data = spec.PayloadAttestationData( + beacon_block_root=block_root, + slot=state.slot, + payload_present=True, + blob_data_available=True, + ) + message = spec.PayloadAttestationMessage( + validator_index=out_of_range_index, + data=data, + signature=spec.BLSSignature(), + ) + yield get_filename(message), message + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + payload_attestation_message=message, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "validator index out of range" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(message), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_payload_attestation_message__ignore_block_not_at_assigned_slot(spec, state): + """A PTC message whose block.slot does not equal data.slot is ignored (assigned slot was empty).""" + yield "topic", "meta", "payload_attestation_message" + + # Apply a block at slot 1, advance state to slot 2 without applying a block + # there. The PTC member for slot 2 would attest against the slot-1 block, + # which the gossip rule must ignore because slot 2 was empty. + store, blocks, block_1_root = setup_store_with_one_block(spec, state) + next_slot(spec, state) + assert state.slot == 2 + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + seen = get_seen(spec) + ptc = spec.get_ptc(state, state.slot) + validator_index = ptc[0] + message = build_payload_attestation_message( + spec, state, state.slot, block_1_root, validator_index + ) + yield get_filename(message), message + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + payload_attestation_message=message, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "message's block is not at the assigned slot" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(message), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_proposer_preferences.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_proposer_preferences.py new file mode 100644 index 0000000000..4730e1e59d --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/networking/test_gossip_proposer_preferences.py @@ -0,0 +1,552 @@ +from eth_consensus_specs.test.context import ( + spec_state_test, + with_gloas_and_later, +) +from eth_consensus_specs.test.helpers.block import build_empty_block_for_next_slot +from eth_consensus_specs.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, +) +from eth_consensus_specs.test.helpers.gloas.proposer_preferences import ( + build_signed_proposer_preferences, + find_upcoming_proposal_slot, +) +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) +from eth_consensus_specs.test.helpers.state import state_transition_and_sign_block + + +def setup_store_with_advanced_state(spec, state, target_slot): + """ + Build a genesis store and advance ``state`` slot-by-slot to ``target_slot``, + adding each intermediate signed block to ``store.blocks`` and the resulting + state to ``store.block_states``. Returns the store and the list of blocks. + """ + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + blocks = [signed_anchor] + while state.slot < target_slot: + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + blocks.append(signed_block) + return store, blocks + + +@with_gloas_and_later +@spec_state_test +def test_gossip_proposer_preferences__valid(spec, state): + """A well-formed SignedProposerPreferences for an upcoming proposal passes gossip.""" + yield "topic", "meta", "proposer_preferences" + + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + store, blocks = setup_store_with_advanced_state(spec, state, target_slot) + yield "state", state + + seen = get_seen(spec) + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + signed_prefs = build_signed_proposer_preferences(spec, state) + yield get_filename(signed_prefs), signed_prefs + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_proposer_preferences__ignore_past_lookahead(spec, state): + """Preferences whose proposal slot is past the proposer lookahead are ignored.""" + yield "topic", "meta", "proposer_preferences" + + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + store, blocks = setup_store_with_advanced_state(spec, state, target_slot) + yield "state", state + + seen = get_seen(spec) + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + # Pick a slot far past the lookahead window. + proposal_slot, validator_index = find_upcoming_proposal_slot(spec, state) + far_future_slot = spec.Slot( + proposal_slot + spec.SLOTS_PER_EPOCH * (spec.MIN_SEED_LOOKAHEAD + 2) + ) + # dependent_root for a far-future slot would underflow get_block_root_at_slot, + # so pass a placeholder; this check fires before any dependent_root lookup. + signed_prefs = build_signed_proposer_preferences( + spec, + state, + proposal_slot=far_future_slot, + validator_index=validator_index, + dependent_root=spec.Root(), + ) + yield get_filename(signed_prefs), signed_prefs + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "proposal slot is past the proposer lookahead" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_proposer_preferences__ignore_already_passed(spec, state): + """Preferences whose proposal slot is already current/past are ignored.""" + yield "topic", "meta", "proposer_preferences" + + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + store, blocks = setup_store_with_advanced_state(spec, state, target_slot) + yield "state", state + + seen = get_seen(spec) + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + signed_prefs = build_signed_proposer_preferences(spec, state) + yield get_filename(signed_prefs), signed_prefs + + # Validate at a time well after the proposal slot has started. + proposal_slot = signed_prefs.message.proposal_slot + time_ms = spec.compute_time_at_slot_ms(state, proposal_slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 1000 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "proposal slot has already passed" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_proposer_preferences__ignore_dependent_root_unseen(spec, state): + """Preferences whose dependent_root has no corresponding block in the store are ignored.""" + yield "topic", "meta", "proposer_preferences" + + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + store, blocks = setup_store_with_advanced_state(spec, state, target_slot) + yield "state", state + + seen = get_seen(spec) + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + unknown_dependent_root = spec.Root(b"\xab" * 32) + signed_prefs = build_signed_proposer_preferences( + spec, state, dependent_root=unknown_dependent_root + ) + yield get_filename(signed_prefs), signed_prefs + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "dependent root block has not been seen" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_proposer_preferences__ignore_duplicate(spec, state): + """The second valid preferences for the same dependent_root and proposal slot is ignored.""" + yield "topic", "meta", "proposer_preferences" + + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + store, blocks = setup_store_with_advanced_state(spec, state, target_slot) + yield "state", state + + seen = get_seen(spec) + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + signed_prefs = build_signed_proposer_preferences(spec, state) + yield get_filename(signed_prefs), signed_prefs + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + # First validation populates seen. + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + } + ) + + # Replay should be ignored. + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "already seen preferences for this dependent root and proposal slot" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_proposer_preferences__reject_wrong_proposer(spec, state): + """Preferences signed by a validator that is not the slot's proposer are rejected.""" + yield "topic", "meta", "proposer_preferences" + + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + store, blocks = setup_store_with_advanced_state(spec, state, target_slot) + yield "state", state + + seen = get_seen(spec) + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + proposal_slot, true_proposer = find_upcoming_proposal_slot(spec, state) + # Pick a different validator that isn't the proposer for this slot. + wrong_index = spec.ValidatorIndex( + next(i for i in range(len(state.validators)) if i != true_proposer) + ) + signed_prefs = build_signed_proposer_preferences( + spec, state, proposal_slot=proposal_slot, validator_index=wrong_index + ) + yield get_filename(signed_prefs), signed_prefs + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "validator is not the proposer for the given slot" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_proposer_preferences__reject_invalid_signature(spec, state): + """Preferences with an invalid signature are rejected.""" + yield "topic", "meta", "proposer_preferences" + + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + store, blocks = setup_store_with_advanced_state(spec, state, target_slot) + yield "state", state + + seen = get_seen(spec) + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + signed_prefs = build_signed_proposer_preferences(spec, state, valid_signature=False) + yield get_filename(signed_prefs), signed_prefs + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "invalid proposer preferences signature" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_proposer_preferences__ignore_before_current_epoch(spec, state): + """Preferences whose proposal slot is in a past epoch are ignored.""" + yield "topic", "meta", "proposer_preferences" + + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + store, blocks = setup_store_with_advanced_state(spec, state, target_slot) + yield "state", state + + seen = get_seen(spec) + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + # Pick a proposal slot whose epoch is strictly less than the current epoch. + past_slot = spec.Slot(0) + _, validator_index = find_upcoming_proposal_slot(spec, state) + # The dependent_root for genesis epoch is unreachable; use a placeholder + # since this check fires before the dependent_root lookup. + signed_prefs = build_signed_proposer_preferences( + spec, + state, + proposal_slot=past_slot, + validator_index=validator_index, + dependent_root=spec.Root(), + ) + yield get_filename(signed_prefs), signed_prefs + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "proposal slot is before the current epoch" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_proposer_preferences__ignore_dependent_root_state_unavailable(spec, state): + """Preferences whose dependent_root has no corresponding state are ignored.""" + yield "topic", "meta", "proposer_preferences" + + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + store, blocks = setup_store_with_advanced_state(spec, state, target_slot) + yield "state", state + + seen = get_seen(spec) + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + + signed_prefs = build_signed_proposer_preferences(spec, state) + # Drop the dependent_root's state but keep the block, so the state-availability + # check fires. + del store.block_states[signed_prefs.message.dependent_root] + yield get_filename(signed_prefs), signed_prefs + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "ignore" + assert reason == "dependent root state is unavailable" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_proposer_preferences__reject_validator_index_out_of_range(spec, state): + """Preferences whose validator index is past the validator registry are rejected.""" + yield "topic", "meta", "proposer_preferences" + + target_slot = spec.compute_start_slot_at_epoch(spec.Epoch(spec.MIN_SEED_LOOKAHEAD + 1)) + store, blocks = setup_store_with_advanced_state(spec, state, target_slot) + seen = get_seen(spec) + + # Build a valid prefs signed by the real proposer for the upcoming slot. + signed_prefs = build_signed_proposer_preferences(spec, state) + validator_index = signed_prefs.message.validator_index + # Trim the head state's validator registry so the proposer's index is now + # past the end. The dependent_root's stored state still has the full + # registry, so is_valid_proposal_slot continues to pass. + state.validators = type(state.validators)(*list(state.validators)[:validator_index]) + yield "state", state + for signed in blocks: + yield get_filename(signed), signed + yield "blocks", "meta", [{"block": get_filename(b)} for b in blocks] + yield get_filename(signed_prefs), signed_prefs + + time_ms = spec.compute_time_at_slot_ms(state, state.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 100 + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_proposer_preferences=signed_prefs, + current_time_ms=time_ms, + ) + assert result == "reject" + assert reason == "validator index out of range" + messages.append( + { + "current_time_ms": int(time_ms), + "message": get_filename(signed_prefs), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/partial-columns/__init__.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/partial-columns/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/partial-columns/test_gossip_partial_data_column_sidecar.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/partial-columns/test_gossip_partial_data_column_sidecar.py new file mode 100644 index 0000000000..77891621c3 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/partial-columns/test_gossip_partial_data_column_sidecar.py @@ -0,0 +1,493 @@ +from eth_consensus_specs.test.context import ( + spec_state_test, + with_gloas_and_later, +) +from eth_consensus_specs.test.helpers.blob import get_block_with_blob_and_sidecars +from eth_consensus_specs.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, +) +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + run_validate_gossip, + wrap_genesis_block, +) + + +def setup_gloas_partial_sidecar(spec, state, blob_indices=None): + """ + Build a signed block carrying one blob, then derive a partial sidecar from + the resulting data column. Returns (store, signed_anchor, signed_block, + partial_sidecar, group_id, column_index). + """ + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + _, _, _, signed_block, sidecars, _ = get_block_with_blob_and_sidecars(spec, state, blob_count=1) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + + sidecar = sidecars[0] + num_blobs = len(sidecar.column) + if blob_indices is None: + blob_indices = list(range(num_blobs)) + bitmap = [i in blob_indices for i in range(num_blobs)] + cells = [sidecar.column[i] for i in blob_indices] + proofs = [sidecar.kzg_proofs[i] for i in blob_indices] + + partial = spec.PartialDataColumnSidecar( + cells_present_bitmap=bitmap, + partial_column=cells, + kzg_proofs=proofs, + ) + group_id = spec.PartialDataColumnGroupID( + slot=sidecar.slot, + beacon_block_root=sidecar.beacon_block_root, + ) + return store, signed_anchor, signed_block, partial, group_id, sidecar.index + + +@with_gloas_and_later +@spec_state_test +def test_gossip_partial_data_column_sidecar__valid(spec, state): + """A well-formed partial sidecar with cells matching the bid passes.""" + yield "topic", "meta", "partial_data_column_sidecar" + + store, signed_anchor, signed_block, partial, group_id, column_index = ( + setup_gloas_partial_sidecar(spec, state) + ) + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(partial), partial + + time_ms = spec.compute_time_at_slot_ms(state, group_id.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + store=store, + sidecar=partial, + group_id=group_id, + column_index=column_index, + ) + assert result == "valid" + assert reason is None + messages.append( + { + "block_root": "0x" + group_id.beacon_block_root.hex(), + "column_index": int(column_index), + "current_time_ms": int(time_ms), + "message": get_filename(partial), + "expected": result, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_partial_data_column_sidecar__reject_empty(spec, state): + """A partial sidecar with no cells set is rejected as semantically empty.""" + yield "topic", "meta", "partial_data_column_sidecar" + + store, signed_anchor, signed_block, partial, group_id, column_index = ( + setup_gloas_partial_sidecar(spec, state, blob_indices=[]) + ) + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(partial), partial + + time_ms = spec.compute_time_at_slot_ms(state, group_id.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + store=store, + sidecar=partial, + group_id=group_id, + column_index=column_index, + ) + assert result == "reject" + assert reason == "partial message is semantically empty" + messages.append( + { + "block_root": "0x" + group_id.beacon_block_root.hex(), + "column_index": int(column_index), + "current_time_ms": int(time_ms), + "message": get_filename(partial), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_partial_data_column_sidecar__reject_slot_mismatch(spec, state): + """A partial sidecar whose group_id.slot doesn't match the block is rejected.""" + yield "topic", "meta", "partial_data_column_sidecar" + + store, signed_anchor, signed_block, partial, group_id, column_index = ( + setup_gloas_partial_sidecar(spec, state) + ) + # Bump the slot so it no longer matches. + group_id.slot = spec.Slot(group_id.slot + 1) + + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(partial), partial + + time_ms = spec.compute_time_at_slot_ms(state, group_id.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + store=store, + sidecar=partial, + group_id=group_id, + column_index=column_index, + ) + assert result == "reject" + assert reason == "group id's slot does not match the block's slot" + messages.append( + { + "block_root": "0x" + group_id.beacon_block_root.hex(), + "column_index": int(column_index), + "current_time_ms": int(time_ms), + "message": get_filename(partial), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_partial_data_column_sidecar__reject_cells_count_mismatch(spec, state): + """A partial sidecar whose cell count differs from the bitmap is rejected.""" + yield "topic", "meta", "partial_data_column_sidecar" + + store, signed_anchor, signed_block, partial, group_id, column_index = ( + setup_gloas_partial_sidecar(spec, state) + ) + partial.partial_column = spec.List[spec.Cell, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( + *partial.partial_column, spec.Cell() + ) + + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(partial), partial + + time_ms = spec.compute_time_at_slot_ms(state, group_id.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + store=store, + sidecar=partial, + group_id=group_id, + column_index=column_index, + ) + assert result == "reject" + assert reason == "number of cells does not match number of set bits" + messages.append( + { + "block_root": "0x" + group_id.beacon_block_root.hex(), + "column_index": int(column_index), + "current_time_ms": int(time_ms), + "message": get_filename(partial), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_partial_data_column_sidecar__reject_proofs_count_mismatch(spec, state): + """A partial sidecar whose proof count differs from the bitmap is rejected.""" + yield "topic", "meta", "partial_data_column_sidecar" + + store, signed_anchor, signed_block, partial, group_id, column_index = ( + setup_gloas_partial_sidecar(spec, state) + ) + partial.kzg_proofs = spec.List[spec.KZGProof, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( + *partial.kzg_proofs, spec.KZGProof() + ) + + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(partial), partial + + time_ms = spec.compute_time_at_slot_ms(state, group_id.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + store=store, + sidecar=partial, + group_id=group_id, + column_index=column_index, + ) + assert result == "reject" + assert reason == "number of proofs does not match number of set bits" + messages.append( + { + "block_root": "0x" + group_id.beacon_block_root.hex(), + "column_index": int(column_index), + "current_time_ms": int(time_ms), + "message": get_filename(partial), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_partial_data_column_sidecar__ignore_block_unseen(spec, state): + """A partial sidecar whose group_id references an unknown block is ignored.""" + yield "topic", "meta", "partial_data_column_sidecar" + + store, signed_anchor, signed_block, partial, group_id, column_index = ( + setup_gloas_partial_sidecar(spec, state) + ) + group_id.beacon_block_root = spec.Root(b"\xab" * 32) + + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(partial), partial + + time_ms = spec.compute_time_at_slot_ms(state, group_id.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + store=store, + sidecar=partial, + group_id=group_id, + column_index=column_index, + ) + assert result == "ignore" + assert reason == "group id's beacon block has not been seen" + messages.append( + { + "block_root": "0x" + group_id.beacon_block_root.hex(), + "column_index": int(column_index), + "current_time_ms": int(time_ms), + "message": get_filename(partial), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_partial_data_column_sidecar__reject_bitmap_length_mismatch(spec, state): + """A partial sidecar whose bitmap length doesn't match the bid's blob count is rejected.""" + yield "topic", "meta", "partial_data_column_sidecar" + + store, signed_anchor, signed_block, partial, group_id, column_index = ( + setup_gloas_partial_sidecar(spec, state) + ) + # Pad bitmap, cells, and proofs in lockstep so the earlier count checks + # pass but the bitmap-vs-bid-commitments check fails. + partial.cells_present_bitmap = spec.List[spec.boolean, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( + *partial.cells_present_bitmap, + spec.boolean(True), # noqa: FBT003 + ) + partial.partial_column = spec.List[spec.Cell, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( + *partial.partial_column, spec.Cell() + ) + partial.kzg_proofs = spec.List[spec.KZGProof, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( + *partial.kzg_proofs, spec.KZGProof() + ) + + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(partial), partial + + time_ms = spec.compute_time_at_slot_ms(state, group_id.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + store=store, + sidecar=partial, + group_id=group_id, + column_index=column_index, + ) + assert result == "reject" + assert reason == "bitmap length does not match the number of bid commitments" + messages.append( + { + "block_root": "0x" + group_id.beacon_block_root.hex(), + "column_index": int(column_index), + "current_time_ms": int(time_ms), + "message": get_filename(partial), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_gloas_and_later +@spec_state_test +def test_gossip_partial_data_column_sidecar__reject_invalid_kzg_proofs(spec, state): + """A partial sidecar whose KZG proofs fail verification is rejected.""" + yield "topic", "meta", "partial_data_column_sidecar" + + # Use two blobs so we can swap proofs to produce verifiable-format but + # incorrect proofs (verify returns False rather than raising). + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + signed_anchor = wrap_genesis_block(spec, anchor_block) + _, _, _, signed_block, sidecars, _ = get_block_with_blob_and_sidecars(spec, state, blob_count=2) + block_root = signed_block.message.hash_tree_root() + store.blocks[block_root] = signed_block.message + store.block_states[block_root] = state.copy() + + sidecar = sidecars[0] + bitmap = [True, True] + cells = [sidecar.column[0], sidecar.column[1]] + # Swap proofs so each cell carries the other cell's proof. + proofs = [sidecar.kzg_proofs[1], sidecar.kzg_proofs[0]] + partial = spec.PartialDataColumnSidecar( + cells_present_bitmap=bitmap, + partial_column=cells, + kzg_proofs=proofs, + ) + group_id = spec.PartialDataColumnGroupID( + slot=sidecar.slot, + beacon_block_root=sidecar.beacon_block_root, + ) + column_index = sidecar.index + + yield "state", state + yield get_filename(signed_anchor), signed_anchor + yield get_filename(signed_block), signed_block + yield ( + "blocks", + "meta", + [ + {"block": get_filename(signed_anchor)}, + {"block": get_filename(signed_block)}, + ], + ) + yield get_filename(partial), partial + + time_ms = spec.compute_time_at_slot_ms(state, group_id.slot) + yield "current_time_ms", "meta", int(time_ms) + messages = [] + + time_ms += 500 + result, reason = run_validate_gossip( + spec, + store=store, + sidecar=partial, + group_id=group_id, + column_index=column_index, + ) + assert result == "reject" + assert reason == "invalid sidecar kzg proofs" + messages.append( + { + "block_root": "0x" + group_id.beacon_block_root.hex(), + "column_index": int(column_index), + "current_time_ms": int(time_ms), + "message": get_filename(partial), + "expected": result, + "reason": reason, + } + ) + + yield "messages", "meta", messages diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/test_is_gas_limit_target_compatible.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/test_is_gas_limit_target_compatible.py deleted file mode 100644 index 46d011ec32..0000000000 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/unittests/test_is_gas_limit_target_compatible.py +++ /dev/null @@ -1,58 +0,0 @@ -from eth_consensus_specs.test.context import ( - single_phase, - spec_test, - with_gloas_and_later, -) - - -@with_gloas_and_later -@spec_test -@single_phase -def test_increase_within_limit(spec): - assert spec.is_gas_limit_target_compatible(60_000_000, 60_000_100, 60_000_100) - - -@with_gloas_and_later -@spec_test -@single_phase -def test_increase_exceeding_limit(spec): - # max_gas_limit_difference = 60_000_000 // 1024 - 1 = 58_592 - assert spec.is_gas_limit_target_compatible(60_000_000, 60_058_592, 100_000_000) - - -@with_gloas_and_later -@spec_test -@single_phase -def test_increase_exceeding_limit_off_by_one_fails(spec): - # gas_limit one above max_gas_limit (= 60_058_592) must fail (off by one) - assert not spec.is_gas_limit_target_compatible(60_000_000, 60_058_593, 100_000_000) - - -@with_gloas_and_later -@spec_test -@single_phase -def test_decrease_within_limit(spec): - assert spec.is_gas_limit_target_compatible(60_000_000, 59_999_990, 59_999_990) - - -@with_gloas_and_later -@spec_test -@single_phase -def test_decrease_exceeding_limit(spec): - # max_gas_limit_difference = 60_000_000 // 1024 - 1 = 58_592 - assert spec.is_gas_limit_target_compatible(60_000_000, 59_941_408, 30_000_000) - - -@with_gloas_and_later -@spec_test -@single_phase -def test_target_equals_parent(spec): - assert spec.is_gas_limit_target_compatible(60_000_000, 60_000_000, 60_000_000) - - -@with_gloas_and_later -@spec_test -@single_phase -def test_parent_gas_limit_underflows(spec): - # parent_gas_limit // 1024 = 0; guard clamps to max(0, 1) - 1 = 0 (no underflow) - assert spec.is_gas_limit_target_compatible(1023, 1023, 60_000_000) diff --git a/tests/core/pyspec/eth_consensus_specs/test/helpers/gloas/proposer_preferences.py b/tests/core/pyspec/eth_consensus_specs/test/helpers/gloas/proposer_preferences.py new file mode 100644 index 0000000000..3a417e4c85 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/gloas/proposer_preferences.py @@ -0,0 +1,62 @@ +from eth_consensus_specs.test.helpers.keys import privkeys + + +def find_upcoming_proposal_slot(spec, state): + """ + Return the next future slot in the proposer lookahead, with the validator + that is proposing it. + """ + current_epoch_start = spec.compute_start_slot_at_epoch(spec.get_current_epoch(state)) + for offset, validator_index in enumerate(state.proposer_lookahead): + slot = spec.Slot(current_epoch_start + offset) + if slot <= state.slot: + continue + return slot, validator_index + raise AssertionError("no upcoming proposal slot found in lookahead") + + +def build_signed_proposer_preferences( + spec, + state, + proposal_slot=None, + validator_index=None, + dependent_root=None, + fee_recipient=None, + target_gas_limit=None, + valid_signature=True, +): + """Construct a SignedProposerPreferences with sensible defaults.""" + if proposal_slot is None or validator_index is None: + proposal_slot, validator_index = find_upcoming_proposal_slot(spec, state) + + if dependent_root is None: + dependent_root = spec.get_proposer_dependent_root( + state, spec.compute_epoch_at_slot(proposal_slot) + ) + + if fee_recipient is None: + fee_recipient = spec.ExecutionAddress(b"\x11" * 20) + + if target_gas_limit is None: + target_gas_limit = spec.uint64(30_000_000) + + preferences = spec.ProposerPreferences( + dependent_root=dependent_root, + proposal_slot=proposal_slot, + validator_index=validator_index, + fee_recipient=fee_recipient, + target_gas_limit=target_gas_limit, + ) + + if valid_signature: + domain = spec.get_domain( + state, + spec.DOMAIN_PROPOSER_PREFERENCES, + spec.compute_epoch_at_slot(proposal_slot), + ) + signing_root = spec.compute_signing_root(preferences, domain) + signature = spec.bls.Sign(privkeys[validator_index], signing_root) + else: + signature = spec.BLSSignature() + + return spec.SignedProposerPreferences(message=preferences, signature=signature) diff --git a/tests/core/pyspec/eth_consensus_specs/test/helpers/gossip.py b/tests/core/pyspec/eth_consensus_specs/test/helpers/gossip.py index a59f173129..107a37c5fa 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/gossip.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/gossip.py @@ -1,13 +1,7 @@ -from eth_utils import encode_hex +import inspect +from typing import get_origin, get_type_hints -from eth_consensus_specs.test.helpers.forks import ( - is_post_altair, - is_post_bellatrix, - is_post_capella, - is_post_deneb, - is_post_fulu, - is_post_gloas, -) +from eth_utils import encode_hex PAYLOAD_STATUS_VALID = "VALID" PAYLOAD_STATUS_INVALIDATED = "INVALIDATED" @@ -33,61 +27,122 @@ def get_spec_block_payload_statuses(spec, block_payload_statuses): return spec_block_payload_statuses -def run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, current_time_ms, block_payload_statuses=None -): - """ - Run validate_beacon_block_gossip and return the result. - Returns: tuple of (result, reason) where result is "valid", "ignore", or "reject" - and reason is the exception message (or None for valid). - """ - kwargs = {} - if is_post_bellatrix(spec): - kwargs["block_payload_statuses"] = get_spec_block_payload_statuses( - spec, block_payload_statuses or {} - ) - try: - spec.validate_beacon_block_gossip( - seen, store, state, signed_block, current_time_ms, **kwargs - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) +_MESSAGE_INFO = { + ########################################################################### + # phase0 + ########################################################################### + "Attestation": { + "file_prefix": "attestation", + "validation_fn": "validate_beacon_attestation_gossip", + }, + "AttesterSlashing": { + "file_prefix": "attester_slashing", + "validation_fn": "validate_attester_slashing_gossip", + }, + "ProposerSlashing": { + "file_prefix": "proposer_slashing", + "validation_fn": "validate_proposer_slashing_gossip", + }, + "SignedAggregateAndProof": { + "file_prefix": "aggregate", + "validation_fn": "validate_beacon_aggregate_and_proof_gossip", + }, + "SignedBeaconBlock": { + "file_prefix": "block", + "validation_fn": "validate_beacon_block_gossip", + }, + "SignedVoluntaryExit": { + "file_prefix": "voluntary_exit", + "validation_fn": "validate_voluntary_exit_gossip", + }, + "SingleAttestation": { + "file_prefix": "single_attestation", + "validation_fn": "validate_beacon_attestation_gossip", + }, + ########################################################################### + # altair + ########################################################################### + "SignedContributionAndProof": { + "file_prefix": "contribution", + "validation_fn": "validate_sync_committee_contribution_and_proof_gossip", + }, + "SyncCommitteeMessage": { + "file_prefix": "sync_committee_message", + "validation_fn": "validate_sync_committee_message_gossip", + }, + ########################################################################### + # capella + ########################################################################### + "SignedBLSToExecutionChange": { + "file_prefix": "bls_to_execution_change", + "validation_fn": "validate_bls_to_execution_change_gossip", + }, + ########################################################################### + # deneb + ########################################################################### + "BlobSidecar": { + "file_prefix": "blob_sidecar", + "validation_fn": "validate_blob_sidecar_gossip", + }, + ########################################################################### + # fulu + ########################################################################### + "DataColumnSidecar": { + "file_prefix": "data_column_sidecar", + "validation_fn": "validate_data_column_sidecar_gossip", + }, + "PartialDataColumnHeader": { + "file_prefix": "partial_data_column_header", + "validation_fn": None, + }, + "PartialDataColumnSidecar": { + "file_prefix": "partial_data_column_sidecar", + "validation_fn": "validate_partial_data_column_sidecar_gossip", + }, + ########################################################################### + # gloas + ########################################################################### + "PayloadAttestationMessage": { + "file_prefix": "payload_attestation_message", + "validation_fn": "validate_payload_attestation_message_gossip", + }, + "SignedExecutionPayloadBid": { + "file_prefix": "execution_payload_bid", + "validation_fn": "validate_execution_payload_bid_gossip", + }, + "SignedExecutionPayloadEnvelope": { + "file_prefix": "execution_payload_envelope", + "validation_fn": "validate_execution_payload_envelope_gossip", + }, + "SignedProposerPreferences": { + "file_prefix": "proposer_preferences", + "validation_fn": "validate_proposer_preferences_gossip", + }, +} -def run_validate_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, subnet_id, current_time_ms -): - """ - Run validate_data_column_sidecar_gossip and return the result. - Returns: tuple of (result, reason) where result is "valid", "ignore", or "reject" - and reason is the exception message (or None for valid). - """ - try: - spec.validate_data_column_sidecar_gossip( - seen, store, state, sidecar, current_time_ms, subnet_id - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) +def get_filename(obj): + """Get a filename for an SSZ object based on its type.""" + class_name = obj.__class__.__name__ + info = _MESSAGE_INFO.get(class_name) + if info is None: + raise Exception(f"unsupported type: {class_name}") + return f"{info['file_prefix']}_{encode_hex(obj.hash_tree_root())}" + +def run_validate_gossip(spec, **kwargs): + """Dispatch to the appropriate gossip validation function based on the message's type.""" + matches = [v for v in kwargs.values() if type(v).__name__ in _MESSAGE_INFO] + assert len(matches) == 1, f"expected exactly one gossip message kwarg, got {len(matches)}" + func_name = _MESSAGE_INFO[type(matches[0]).__name__]["validation_fn"] + if func_name is None: + raise Exception(f"unsupported gossip message type: {type(matches[0]).__name__}") + spec_func = getattr(spec, func_name) + extras = set(kwargs) - set(inspect.signature(spec_func).parameters) + assert not extras, f"unexpected kwargs for {func_name}: {sorted(extras)}" -def run_validate_partial_data_column_sidecar_gossip( - spec, seen, store, state, sidecar, block_root, column_index, current_time_ms -): - """ - Run validate_partial_data_column_sidecar_gossip and return the result. - Returns: tuple of (result, reason) where result is "valid", "ignore", or "reject" - and reason is the exception message (or None for valid). - """ try: - spec.validate_partial_data_column_sidecar_gossip( - seen, store, state, sidecar, block_root, column_index, current_time_ms - ) + spec_func(**kwargs) return "valid", None except spec.GossipIgnore as e: return "ignore", str(e) @@ -96,89 +151,5 @@ def run_validate_partial_data_column_sidecar_gossip( def get_seen(spec): - """Create an empty Seen object for gossip validation.""" - kwargs = { - "proposer_slots": set(), - "aggregator_epochs": set(), - "aggregate_data_roots": {}, - "voluntary_exit_indices": set(), - "proposer_slashing_indices": set(), - "attester_slashing_indices": set(), - "attestation_validator_epochs": set(), - } - if is_post_altair(spec): - kwargs.update( - { - "sync_contribution_aggregator_slots": set(), - "sync_contribution_data": {}, - "sync_message_validator_slots": set(), - } - ) - if is_post_capella(spec): - kwargs.update( - { - "bls_to_execution_change_indices": set(), - } - ) - if is_post_deneb(spec) and not is_post_fulu(spec): - kwargs.update( - { - "blob_sidecar_tuples": set(), - } - ) - if is_post_fulu(spec): - kwargs.update( - { - "data_column_sidecar_tuples": set(), - } - ) - if not is_post_gloas(spec): - kwargs.update( - { - "partial_data_column_headers": {}, - } - ) - return spec.Seen(**kwargs) - - -def get_filename(obj): - """Get a filename for an SSZ object based on its type.""" - class_name = obj.__class__.__name__ - - # phase0 - if "BeaconBlock" in class_name: - prefix = "block" - elif class_name == "Attestation": - prefix = "attestation" - elif class_name == "SingleAttestation": - prefix = "single_attestation" - elif "AggregateAndProof" in class_name: - prefix = "aggregate" - elif class_name == "ProposerSlashing": - prefix = "proposer_slashing" - elif class_name == "AttesterSlashing": - prefix = "attester_slashing" - elif "VoluntaryExit" in class_name: - prefix = "voluntary_exit" - # altair - elif "ContributionAndProof" in class_name: - prefix = "contribution" - elif class_name == "SyncCommitteeMessage": - prefix = "sync_committee_message" - # capella - elif "BLSToExecutionChange" in class_name: - prefix = "bls_to_execution_change" - # deneb - elif class_name == "BlobSidecar": - prefix = "blob_sidecar" - # fulu - elif class_name == "DataColumnSidecar": - prefix = "data_column_sidecar" - elif class_name == "PartialDataColumnHeader": - prefix = "partial_data_column_header" - elif class_name == "PartialDataColumnSidecar": - prefix = "partial_data_column_sidecar" - else: - raise Exception(f"unsupported type: {class_name}") - - return f"{prefix}_{encode_hex(obj.hash_tree_root())}" + """Create an empty Seen object by instantiating each annotated field's container type.""" + return spec.Seen(**{name: get_origin(t)() for name, t in get_type_hints(spec.Seen).items()}) diff --git a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_attester_slashing.py b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_attester_slashing.py index 9282a53ec0..8da11dd4bc 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_attester_slashing.py +++ b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_attester_slashing.py @@ -6,22 +6,7 @@ from eth_consensus_specs.test.helpers.attester_slashings import ( get_valid_attester_slashing, ) -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen - - -def run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing): - """ - Run validate_attester_slashing_gossip and return the result. - Returns: tuple of (result, reason) where result is "valid", "ignore", or "reject" - and reason is the exception message (or None for valid). - """ - try: - spec.validate_attester_slashing_gossip(seen, state, attester_slashing) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) +from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, run_validate_gossip @with_all_phases @@ -40,7 +25,9 @@ def test_gossip_attester_slashing__valid(spec, state): yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "valid" assert reason is None @@ -69,13 +56,17 @@ def test_gossip_attester_slashing__ignore_already_seen(spec, state): yield get_filename(attester_slashing), attester_slashing # First validation should pass - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "valid" assert reason is None messages.append({"message": get_filename(attester_slashing), "expected": "valid"}) # Second validation should be ignored (all indices already seen) - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "ignore" assert reason == "all attester slashing indices already seen" messages.append( @@ -108,7 +99,9 @@ def test_gossip_attester_slashing__reject_not_slashable_data(spec, state): yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "reject" assert reason == "attestation data is not slashable" @@ -142,7 +135,9 @@ def test_gossip_attester_slashing__reject_invalid_attestation_1(spec, state): yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "reject" assert reason == "invalid indexed attestation 1" @@ -176,7 +171,9 @@ def test_gossip_attester_slashing__reject_invalid_attestation_2(spec, state): yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "reject" assert reason == "invalid indexed attestation 2" @@ -212,7 +209,9 @@ def test_gossip_attester_slashing__reject_attesting_index_out_of_range_1(spec, s yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "reject" assert reason == "validator index out of range in indexed attestation 1" @@ -248,7 +247,9 @@ def test_gossip_attester_slashing__reject_attesting_index_out_of_range_2(spec, s yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "reject" assert reason == "validator index out of range in indexed attestation 2" @@ -281,7 +282,9 @@ def test_gossip_attester_slashing__ignore_empty_attesting_indices_1(spec, state) yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "ignore" assert reason == "all attester slashing indices already seen" @@ -314,7 +317,9 @@ def test_gossip_attester_slashing__ignore_empty_attesting_indices_2(spec, state) yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "ignore" assert reason == "all attester slashing indices already seen" @@ -355,7 +360,9 @@ def test_gossip_attester_slashing__reject_unsorted_indices_1(spec, state): yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "reject" assert reason == "invalid indexed attestation 1" @@ -391,7 +398,9 @@ def test_gossip_attester_slashing__reject_unsorted_indices_2(spec, state): yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "reject" assert reason == "invalid indexed attestation 2" @@ -431,7 +440,9 @@ def test_gossip_attester_slashing__reject_no_slashable_validators(spec, state): yield "state", state yield get_filename(attester_slashing), attester_slashing - result, reason = run_validate_attester_slashing_gossip(spec, seen, state, attester_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, attester_slashing=attester_slashing + ) assert result == "reject" assert reason == "no slashable validators in intersection" diff --git a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_aggregate_and_proof.py b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_aggregate_and_proof.py index b461d590d6..1d0991def7 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_aggregate_and_proof.py +++ b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_aggregate_and_proof.py @@ -4,8 +4,9 @@ single_phase, spec_state_test, spec_test, + with_all_phases, + with_all_phases_from_to, with_custom_state, - with_phases, with_presets, ) from eth_consensus_specs.test.helpers.attestations import ( @@ -16,20 +17,20 @@ build_empty_block_for_next_slot, ) from eth_consensus_specs.test.helpers.constants import ( - ALTAIR, - BELLATRIX, - CAPELLA, DENEB, - ELECTRA, - FULU, MAINNET, PHASE0, ) from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) -from eth_consensus_specs.test.helpers.forks import is_post_electra -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, wrap_genesis_block +from eth_consensus_specs.test.helpers.forks import is_post_electra, is_post_gloas +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.test.helpers.state import ( next_slot, @@ -77,26 +78,7 @@ def create_signed_aggregate_and_proof(spec, state, attestation, aggregator_index ) -def run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_aggregate_and_proof, current_time_ms -): - """ - Run validate_beacon_aggregate_and_proof_gossip and return the result. - Returns: tuple of (result, reason) where result is "valid", "ignore", or "reject" - and reason is the exception message (or None for valid). - """ - try: - spec.validate_beacon_aggregate_and_proof_gossip( - seen, store, state, signed_aggregate_and_proof, current_time_ms - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__valid(spec, state): """ @@ -124,8 +106,17 @@ def test_gossip_beacon_aggregate_and_proof__valid(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "valid" assert reason is None @@ -137,7 +128,7 @@ def test_gossip_beacon_aggregate_and_proof__valid(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_committee_index_out_of_range(spec, state): """ @@ -179,8 +170,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_committee_index_out_of_range( yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "committee index out of range" @@ -199,7 +199,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_committee_index_out_of_range( ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) +@with_all_phases_from_to(PHASE0, DENEB) @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_slot_not_within_range(spec, state): """ @@ -229,8 +229,17 @@ def test_gossip_beacon_aggregate_and_proof__ignore_slot_not_within_range(spec, s yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "ignore" assert reason == "attestation slot not within propagation range" @@ -249,7 +258,7 @@ def test_gossip_beacon_aggregate_and_proof__ignore_slot_not_within_range(spec, s ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__valid_within_clock_disparity(spec, state): """ @@ -279,8 +288,17 @@ def test_gossip_beacon_aggregate_and_proof__valid_within_clock_disparity(spec, s yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "valid" assert reason is None @@ -298,7 +316,7 @@ def test_gossip_beacon_aggregate_and_proof__valid_within_clock_disparity(spec, s ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_epoch_mismatch(spec, state): """ @@ -329,8 +347,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_epoch_mismatch(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "attestation epoch does not match target epoch" @@ -349,7 +376,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_epoch_mismatch(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregate(spec, state): """ @@ -379,15 +406,33 @@ def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregate(spec, yield "current_time_ms", "meta", int(block_time_ms) # First validation should pass - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "valid" messages.append({"offset_ms": 500, "message": get_filename(signed_agg), "expected": "valid"}) # Second validation should be ignored (already seen aggregate data) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 600 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 600, + **kwargs, ) assert result == "ignore" assert reason == "already seen aggregate for this data" @@ -403,7 +448,7 @@ def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregate(spec, yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_same_data_root_without_superset(spec, state): """ @@ -440,8 +485,17 @@ def test_gossip_beacon_aggregate_and_proof__ignore_same_data_root_without_supers yield "current_time_ms", "meta", int(block_time_ms) # First validation should pass and seed dedup state. - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg_1, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg_1, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "valid" assert reason is None @@ -490,8 +544,17 @@ def test_gossip_beacon_aggregate_and_proof__ignore_same_data_root_without_supers # Dedup should not trigger here; the ignore result is from the aggregator # uniqueness rule because aggregator/epoch are unchanged. - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg_2, block_time_ms + 600 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg_2, + current_time_ms=block_time_ms + 600, + **kwargs, ) assert result == "ignore" assert reason == "already seen aggregate from this aggregator for this epoch" @@ -507,7 +570,7 @@ def test_gossip_beacon_aggregate_and_proof__ignore_same_data_root_without_supers yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__valid_two_aggregators_same_data(spec, state): """ @@ -573,16 +636,34 @@ def test_gossip_beacon_aggregate_and_proof__valid_two_aggregators_same_data(spec yield "current_time_ms", "meta", int(block_time_ms) # First aggregate should pass - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg_1, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg_1, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "valid" assert reason is None messages.append({"offset_ms": 500, "message": get_filename(signed_agg_1), "expected": "valid"}) # Second aggregate (different aggregator, same data root) should also pass - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg_2, block_time_ms + 600 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg_2, + current_time_ms=block_time_ms + 600, + **kwargs, ) assert result == "valid" assert reason is None @@ -591,7 +672,7 @@ def test_gossip_beacon_aggregate_and_proof__valid_two_aggregators_same_data(spec yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_block_not_seen(spec, state): """ @@ -625,8 +706,17 @@ def test_gossip_beacon_aggregate_and_proof__ignore_block_not_seen(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "ignore" assert reason == "block being voted for has not been seen" @@ -645,7 +735,7 @@ def test_gossip_beacon_aggregate_and_proof__ignore_block_not_seen(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_aggregation_bits_size_mismatch(spec, state): """ @@ -682,8 +772,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_aggregation_bits_size_mismatc yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "aggregation bits length does not match committee size" @@ -702,7 +801,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_aggregation_bits_size_mismatc ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_no_participants(spec, state): """ @@ -737,8 +836,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_no_participants(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "aggregate has no participants" @@ -757,7 +865,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_no_participants(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregator(spec, state): """ @@ -787,8 +895,17 @@ def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregator(spec, yield "current_time_ms", "meta", int(block_time_ms) # First validation should pass - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg1, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg1, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "valid" messages.append({"offset_ms": 500, "message": get_filename(signed_agg1), "expected": "valid"}) @@ -803,8 +920,17 @@ def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregator(spec, yield get_filename(signed_agg2), signed_agg2 # Second validation should be ignored (same aggregator, same epoch) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg2, block_time_ms + 600 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg2, + current_time_ms=block_time_ms + 600, + **kwargs, ) assert result == "ignore" assert reason == "already seen aggregate from this aggregator for this epoch" @@ -820,7 +946,7 @@ def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregator(spec, yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @with_presets([MAINNET], reason="minimal preset has committees < 16, so everyone is an aggregator") @spec_test @with_custom_state( @@ -889,8 +1015,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_not_aggregator(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "validator is not selected as aggregator" @@ -909,7 +1044,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_not_aggregator(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_aggregator_not_in_committee(spec, state): """ @@ -948,8 +1083,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_aggregator_not_in_committee(s yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "aggregator index not in committee" @@ -968,7 +1112,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_aggregator_not_in_committee(s ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_aggregator_index_out_of_range(spec, state): """ @@ -998,8 +1142,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_aggregator_index_out_of_range yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "aggregator index not in committee" @@ -1018,7 +1171,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_aggregator_index_out_of_range ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test @always_bls def test_gossip_beacon_aggregate_and_proof__reject_invalid_selection_proof(spec, state): @@ -1050,8 +1203,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_invalid_selection_proof(spec, yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "invalid selection proof signature" @@ -1070,7 +1232,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_invalid_selection_proof(spec, ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test @always_bls def test_gossip_beacon_aggregate_and_proof__reject_invalid_aggregator_signature(spec, state): @@ -1102,8 +1264,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_invalid_aggregator_signature( yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "invalid aggregator signature" @@ -1122,7 +1293,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_invalid_aggregator_signature( ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test @always_bls def test_gossip_beacon_aggregate_and_proof__reject_invalid_aggregate_signature(spec, state): @@ -1154,8 +1325,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_invalid_aggregate_signature(s yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "invalid aggregate signature" @@ -1174,7 +1354,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_invalid_aggregate_signature(s ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_block_failed_validation(spec, state): """ @@ -1218,8 +1398,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_block_failed_validation(spec, yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "block being voted for failed validation" @@ -1238,7 +1427,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_block_failed_validation(spec, ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_target_not_ancestor(spec, state): """ @@ -1270,8 +1459,17 @@ def test_gossip_beacon_aggregate_and_proof__reject_target_not_ancestor(spec, sta yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "target block is not an ancestor of LMD vote block" @@ -1290,7 +1488,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_target_not_ancestor(spec, sta ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_finalized_not_ancestor(spec, state): """ @@ -1326,8 +1524,17 @@ def test_gossip_beacon_aggregate_and_proof__ignore_finalized_not_ancestor(spec, yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_aggregate_and_proof_gossip( - spec, seen, store, state, signed_agg, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_aggregate_and_proof=signed_agg, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "ignore" assert reason == "finalized checkpoint is not an ancestor of block" diff --git a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_attestation.py b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_attestation.py index fb974b0443..42e97af722 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_attestation.py +++ b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_attestation.py @@ -1,7 +1,8 @@ from eth_consensus_specs.test.context import ( always_bls, spec_state_test, - with_phases, + with_all_phases, + with_all_phases_from_to, ) from eth_consensus_specs.test.helpers.attestations import ( get_valid_attestation, @@ -11,19 +12,20 @@ build_empty_block_for_next_slot, ) from eth_consensus_specs.test.helpers.constants import ( - ALTAIR, - BELLATRIX, - CAPELLA, DENEB, ELECTRA, - FULU, PHASE0, ) from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) -from eth_consensus_specs.test.helpers.forks import is_post_electra -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, wrap_genesis_block +from eth_consensus_specs.test.helpers.forks import is_post_electra, is_post_gloas +from eth_consensus_specs.test.helpers.gossip import ( + get_filename, + get_seen, + run_validate_gossip, + wrap_genesis_block, +) from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.test.helpers.state import ( next_slot, @@ -42,26 +44,7 @@ def get_correct_subnet_for_attestation(spec, state, attestation): ) -def run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms -): - """ - Run validate_beacon_attestation_gossip and return the result. - Returns: tuple of (result, reason) where result is "valid", "ignore", or "reject" - and reason is the exception message (or None for valid). - """ - try: - spec.validate_beacon_attestation_gossip( - seen, store, state, attestation, current_time_ms, subnet_id - ) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_attestation__valid(spec, state): """ @@ -104,8 +87,18 @@ def test_gossip_beacon_attestation__valid(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -124,7 +117,7 @@ def test_gossip_beacon_attestation__valid(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_attestation__reject_committee_index_out_of_range(spec, state): """ @@ -160,8 +153,18 @@ def test_gossip_beacon_attestation__reject_committee_index_out_of_range(spec, st yield "current_time_ms", "meta", int(block_time_ms) subnet_id = spec.uint64(0) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "reject" assert reason == "committee index out of range" @@ -181,7 +184,7 @@ def test_gossip_beacon_attestation__reject_committee_index_out_of_range(spec, st ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_attestation__reject_wrong_subnet(spec, state): """ @@ -213,8 +216,18 @@ def test_gossip_beacon_attestation__reject_wrong_subnet(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, wrong_subnet, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=wrong_subnet, + **kwargs, ) assert result == "reject" assert reason == "attestation is for wrong subnet" @@ -234,7 +247,7 @@ def test_gossip_beacon_attestation__reject_wrong_subnet(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) +@with_all_phases_from_to(PHASE0, DENEB) @spec_state_test def test_gossip_beacon_attestation__ignore_slot_not_in_range(spec, state): """ @@ -274,8 +287,18 @@ def test_gossip_beacon_attestation__ignore_slot_not_in_range(spec, state): yield "current_time_ms", "meta", int(current_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "ignore" assert reason == "attestation slot not within propagation range" @@ -295,7 +318,7 @@ def test_gossip_beacon_attestation__ignore_slot_not_in_range(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_attestation__valid_within_clock_disparity(spec, state): """ @@ -338,8 +361,18 @@ def test_gossip_beacon_attestation__valid_within_clock_disparity(spec, state): yield "current_time_ms", "meta", int(current_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -358,7 +391,7 @@ def test_gossip_beacon_attestation__valid_within_clock_disparity(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) +@with_all_phases_from_to(PHASE0, DENEB) @spec_state_test def test_gossip_beacon_attestation__valid_within_clock_disparity_old(spec, state): """ @@ -400,8 +433,18 @@ def test_gossip_beacon_attestation__valid_within_clock_disparity_old(spec, state yield "current_time_ms", "meta", int(current_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -420,7 +463,7 @@ def test_gossip_beacon_attestation__valid_within_clock_disparity_old(spec, state ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) +@with_all_phases_from_to(PHASE0, DENEB) @spec_state_test def test_gossip_beacon_attestation__ignore_slot_too_old(spec, state): """ @@ -462,8 +505,18 @@ def test_gossip_beacon_attestation__ignore_slot_too_old(spec, state): yield "current_time_ms", "meta", int(current_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, current_time_ms + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=current_time_ms, + subnet_id=subnet_id, + **kwargs, ) assert result == "ignore" assert reason == "attestation slot not within propagation range" @@ -483,7 +536,7 @@ def test_gossip_beacon_attestation__ignore_slot_too_old(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_attestation__reject_epoch_mismatch(spec, state): """ @@ -517,8 +570,18 @@ def test_gossip_beacon_attestation__reject_epoch_mismatch(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "reject" assert reason == "attestation epoch does not match target epoch" @@ -538,7 +601,7 @@ def test_gossip_beacon_attestation__reject_epoch_mismatch(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB]) +@with_all_phases_from_to(PHASE0, ELECTRA) @spec_state_test def test_gossip_beacon_attestation__reject_not_unaggregated(spec, state): """ @@ -575,8 +638,18 @@ def test_gossip_beacon_attestation__reject_not_unaggregated(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "reject" assert reason == "attestation is not unaggregated" @@ -596,7 +669,7 @@ def test_gossip_beacon_attestation__reject_not_unaggregated(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB]) +@with_all_phases_from_to(PHASE0, ELECTRA) @spec_state_test def test_gossip_beacon_attestation__reject_aggregation_bits_size_mismatch(spec, state): """ @@ -631,8 +704,18 @@ def test_gossip_beacon_attestation__reject_aggregation_bits_size_mismatch(spec, yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "reject" assert reason == "aggregation bits length does not match committee size" @@ -652,7 +735,7 @@ def test_gossip_beacon_attestation__reject_aggregation_bits_size_mismatch(spec, ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_attestation__ignore_already_seen(spec, state): """ @@ -695,8 +778,18 @@ def test_gossip_beacon_attestation__ignore_already_seen(spec, state): # First validation should pass subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "valid" assert reason is None @@ -710,8 +803,18 @@ def test_gossip_beacon_attestation__ignore_already_seen(spec, state): ) # Second validation should be ignored - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 600 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 600, + subnet_id=subnet_id, + **kwargs, ) assert result == "ignore" assert reason == "already seen attestation from this validator for this epoch" @@ -728,7 +831,7 @@ def test_gossip_beacon_attestation__ignore_already_seen(spec, state): yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_attestation__ignore_block_not_seen(spec, state): """ @@ -772,8 +875,18 @@ def test_gossip_beacon_attestation__ignore_block_not_seen(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "ignore" assert reason == "block being voted for has not been seen" @@ -793,7 +906,7 @@ def test_gossip_beacon_attestation__ignore_block_not_seen(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_attestation__reject_block_failed_validation(spec, state): """ @@ -850,8 +963,18 @@ def test_gossip_beacon_attestation__reject_block_failed_validation(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "reject" assert reason == "block being voted for failed validation" @@ -871,7 +994,7 @@ def test_gossip_beacon_attestation__reject_block_failed_validation(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test @always_bls def test_gossip_beacon_attestation__reject_invalid_signature(spec, state): @@ -912,8 +1035,18 @@ def test_gossip_beacon_attestation__reject_invalid_signature(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "reject" assert reason == "invalid attestation signature" @@ -933,7 +1066,7 @@ def test_gossip_beacon_attestation__reject_invalid_signature(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_attestation__reject_target_not_ancestor(spec, state): """ @@ -977,8 +1110,18 @@ def test_gossip_beacon_attestation__reject_target_not_ancestor(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "reject" assert reason == "target block is not an ancestor of LMD vote block" @@ -998,7 +1141,7 @@ def test_gossip_beacon_attestation__reject_target_not_ancestor(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_attestation__ignore_finalized_not_ancestor(spec, state): """ @@ -1064,8 +1207,18 @@ def test_gossip_beacon_attestation__ignore_finalized_not_ancestor(spec, state): yield "current_time_ms", "meta", int(block_time_ms) subnet_id = get_correct_subnet_for_attestation(spec, state, attestation) - result, reason = run_validate_beacon_attestation_gossip( - spec, seen, store, state, attestation, subnet_id, block_time_ms + 500 + kwargs = {} + if is_post_gloas(spec): + kwargs["block_payload_statuses"] = {} + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + attestation=attestation, + current_time_ms=block_time_ms + 500, + subnet_id=subnet_id, + **kwargs, ) assert result == "ignore" assert reason == "finalized checkpoint is not an ancestor of block" diff --git a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_block.py b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_block.py index d605ceba82..d90bcdcdb3 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_block.py +++ b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_beacon_block.py @@ -1,19 +1,16 @@ from eth_consensus_specs.test.context import ( always_bls, spec_state_test, - with_phases, + with_all_phases, + with_all_phases_from_to, ) from eth_consensus_specs.test.helpers.block import ( build_empty_block_for_next_slot, sign_block, ) from eth_consensus_specs.test.helpers.constants import ( - ALTAIR, - BELLATRIX, CAPELLA, - DENEB, - ELECTRA, - FULU, + GLOAS, PHASE0, ) from eth_consensus_specs.test.helpers.execution_payload import ( @@ -23,11 +20,11 @@ from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) -from eth_consensus_specs.test.helpers.forks import is_post_bellatrix +from eth_consensus_specs.test.helpers.forks import is_post_bellatrix, is_post_gloas from eth_consensus_specs.test.helpers.gossip import ( get_filename, get_seen, - run_validate_beacon_block_gossip, + run_validate_gossip, wrap_genesis_block, ) from eth_consensus_specs.test.helpers.state import ( @@ -35,7 +32,7 @@ ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_block__valid_block(spec, state): """ @@ -60,8 +57,19 @@ def test_gossip_beacon_block__valid_block(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "valid" assert reason is None @@ -73,7 +81,7 @@ def test_gossip_beacon_block__valid_block(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_block__ignore_future_slot(spec, state): """ @@ -99,8 +107,19 @@ def test_gossip_beacon_block__ignore_future_slot(spec, state): yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, current_time_ms + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "ignore" assert reason == "block is from a future slot" @@ -119,7 +138,7 @@ def test_gossip_beacon_block__ignore_future_slot(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_block__valid_within_clock_disparity(spec, state): """ @@ -145,8 +164,19 @@ def test_gossip_beacon_block__valid_within_clock_disparity(spec, state): yield "current_time_ms", "meta", int(current_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, current_time_ms + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=current_time_ms, + **kwargs, ) assert result == "valid" assert reason is None @@ -158,7 +188,7 @@ def test_gossip_beacon_block__valid_within_clock_disparity(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_block__ignore_already_seen_proposer_slot(spec, state): """ @@ -185,16 +215,38 @@ def test_gossip_beacon_block__ignore_already_seen_proposer_slot(spec, state): yield "current_time_ms", "meta", int(block_time_ms) # First block should be valid - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "valid" assert reason is None messages.append({"offset_ms": 500, "message": get_filename(signed_block), "expected": "valid"}) # Second block with same proposer/slot should be ignored - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 600 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 600, + **kwargs, ) assert result == "ignore" assert reason == "block is not the first valid block for this proposer and slot" @@ -210,7 +262,7 @@ def test_gossip_beacon_block__ignore_already_seen_proposer_slot(spec, state): yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_block__ignore_slot_not_greater_than_finalized(spec, state): """ @@ -262,8 +314,19 @@ def test_gossip_beacon_block__ignore_slot_not_greater_than_finalized(spec, state yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "ignore" assert reason == "block is not from a slot greater than the latest finalized slot" @@ -282,7 +345,7 @@ def test_gossip_beacon_block__ignore_slot_not_greater_than_finalized(spec, state ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_block__ignore_parent_not_seen(spec, state): """ @@ -317,8 +380,19 @@ def test_gossip_beacon_block__ignore_parent_not_seen(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "ignore" assert reason == "block's parent has not been seen" @@ -337,7 +411,7 @@ def test_gossip_beacon_block__ignore_parent_not_seen(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_all_phases_from_to(PHASE0, CAPELLA) @spec_state_test def test_gossip_beacon_block__reject_parent_failed_validation(spec, state): """ @@ -403,8 +477,19 @@ def test_gossip_beacon_block__reject_parent_failed_validation(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_child, block_time_ms + 500 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_child, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" if is_post_bellatrix(spec): @@ -426,7 +511,7 @@ def test_gossip_beacon_block__reject_parent_failed_validation(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases_from_to(PHASE0, GLOAS) @spec_state_test def test_gossip_beacon_block__reject_slot_not_higher_than_parent(spec, state): """ @@ -480,8 +565,19 @@ def test_gossip_beacon_block__reject_slot_not_higher_than_parent(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "block is not from a higher slot than its parent" @@ -500,7 +596,7 @@ def test_gossip_beacon_block__reject_slot_not_higher_than_parent(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases_from_to(PHASE0, GLOAS) @spec_state_test def test_gossip_beacon_block__reject_finalized_checkpoint_not_ancestor(spec, state): """ @@ -565,8 +661,19 @@ def test_gossip_beacon_block__reject_finalized_checkpoint_not_ancestor(spec, sta yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_child, block_time_ms + 500 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_child, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "finalized checkpoint is not an ancestor of block" @@ -585,7 +692,7 @@ def test_gossip_beacon_block__reject_finalized_checkpoint_not_ancestor(spec, sta ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test @always_bls def test_gossip_beacon_block__reject_invalid_proposer_signature(spec, state): @@ -614,8 +721,19 @@ def test_gossip_beacon_block__reject_invalid_proposer_signature(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "invalid proposer signature" @@ -634,7 +752,7 @@ def test_gossip_beacon_block__reject_invalid_proposer_signature(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_beacon_block__reject_invalid_proposer_index(spec, state): """ @@ -662,8 +780,19 @@ def test_gossip_beacon_block__reject_invalid_proposer_index(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "proposer index out of range" @@ -682,7 +811,7 @@ def test_gossip_beacon_block__reject_invalid_proposer_index(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases_from_to(PHASE0, GLOAS) @spec_state_test def test_gossip_beacon_block__reject_wrong_proposer_index(spec, state): """ @@ -716,8 +845,19 @@ def test_gossip_beacon_block__reject_wrong_proposer_index(spec, state): yield "current_time_ms", "meta", int(block_time_ms) - result, reason = run_validate_beacon_block_gossip( - spec, seen, store, state, signed_block, block_time_ms + 500 + kwargs = ( + {"block_payload_statuses": {}} + if is_post_bellatrix(spec) and not is_post_gloas(spec) + else {} + ) + result, reason = run_validate_gossip( + spec, + seen=seen, + store=store, + state=state, + signed_beacon_block=signed_block, + current_time_ms=block_time_ms + 500, + **kwargs, ) assert result == "reject" assert reason == "block proposer_index does not match expected proposer" diff --git a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_proposer_slashing.py b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_proposer_slashing.py index 29ec5a800e..34170c5a29 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_proposer_slashing.py +++ b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_proposer_slashing.py @@ -3,27 +3,12 @@ spec_state_test, with_all_phases, ) -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen +from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, run_validate_gossip from eth_consensus_specs.test.helpers.proposer_slashings import ( get_valid_proposer_slashing, ) -def run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing): - """ - Run validate_proposer_slashing_gossip and return the result. - Returns: tuple of (result, reason) where result is "valid", "ignore", or "reject" - and reason is the exception message (or None for valid). - """ - try: - spec.validate_proposer_slashing_gossip(seen, state, proposer_slashing) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - @with_all_phases @spec_state_test def test_gossip_proposer_slashing__valid(spec, state): @@ -40,7 +25,9 @@ def test_gossip_proposer_slashing__valid(spec, state): yield get_filename(proposer_slashing), proposer_slashing - result, reason = run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, proposer_slashing=proposer_slashing + ) assert result == "valid" assert reason is None @@ -69,13 +56,17 @@ def test_gossip_proposer_slashing__ignore_already_seen(spec, state): yield get_filename(proposer_slashing), proposer_slashing # First validation should pass - result, reason = run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, proposer_slashing=proposer_slashing + ) assert result == "valid" assert reason is None messages.append({"message": get_filename(proposer_slashing), "expected": "valid"}) # Second validation should be ignored - result, reason = run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, proposer_slashing=proposer_slashing + ) assert result == "ignore" assert reason == "already seen proposer slashing for this proposer" messages.append( @@ -110,7 +101,9 @@ def test_gossip_proposer_slashing__reject_slots_not_matching(spec, state): yield get_filename(proposer_slashing), proposer_slashing - result, reason = run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, proposer_slashing=proposer_slashing + ) assert result == "reject" assert reason == "header slots do not match" @@ -148,7 +141,9 @@ def test_gossip_proposer_slashing__reject_proposer_indices_not_matching(spec, st yield get_filename(proposer_slashing), proposer_slashing - result, reason = run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, proposer_slashing=proposer_slashing + ) assert result == "reject" assert reason == "header proposer indices do not match" @@ -184,7 +179,9 @@ def test_gossip_proposer_slashing__reject_headers_identical(spec, state): yield get_filename(proposer_slashing), proposer_slashing - result, reason = run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, proposer_slashing=proposer_slashing + ) assert result == "reject" assert reason == "headers are not different" @@ -222,7 +219,9 @@ def test_gossip_proposer_slashing__reject_proposer_index_out_of_range(spec, stat yield get_filename(proposer_slashing), proposer_slashing - result, reason = run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, proposer_slashing=proposer_slashing + ) assert result == "reject" assert reason == "proposer index out of range" @@ -259,7 +258,9 @@ def test_gossip_proposer_slashing__reject_proposer_not_slashable(spec, state): yield "state", state yield get_filename(proposer_slashing), proposer_slashing - result, reason = run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, proposer_slashing=proposer_slashing + ) assert result == "reject" assert reason == "proposer is not slashable" @@ -293,7 +294,9 @@ def test_gossip_proposer_slashing__reject_invalid_signature_1(spec, state): yield get_filename(proposer_slashing), proposer_slashing - result, reason = run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, proposer_slashing=proposer_slashing + ) assert result == "reject" assert reason == "invalid proposer slashing signature" @@ -327,7 +330,9 @@ def test_gossip_proposer_slashing__reject_invalid_signature_2(spec, state): yield get_filename(proposer_slashing), proposer_slashing - result, reason = run_validate_proposer_slashing_gossip(spec, seen, state, proposer_slashing) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, proposer_slashing=proposer_slashing + ) assert result == "reject" assert reason == "invalid proposer slashing signature" diff --git a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_voluntary_exit.py b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_voluntary_exit.py index a8de66f9cd..3745e9f7e1 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_voluntary_exit.py +++ b/tests/core/pyspec/eth_consensus_specs/test/phase0/networking/test_gossip_voluntary_exit.py @@ -1,18 +1,9 @@ from eth_consensus_specs.test.context import ( always_bls, spec_state_test, - with_phases, + with_all_phases, ) -from eth_consensus_specs.test.helpers.constants import ( - ALTAIR, - BELLATRIX, - CAPELLA, - DENEB, - ELECTRA, - FULU, - PHASE0, -) -from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen +from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen, run_validate_gossip from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.test.helpers.state import ( next_epoch_via_block, @@ -36,22 +27,7 @@ def create_signed_voluntary_exit(spec, state, validator_index, epoch=None): return sign_voluntary_exit(spec, state, voluntary_exit, privkeys[validator_index]) -def run_validate_voluntary_exit_gossip(spec, seen, state, signed_voluntary_exit): - """ - Run validate_voluntary_exit_gossip and return the result. - Returns: tuple of (result, reason) where result is "valid", "ignore", or "reject" - and reason is the exception message (or None for valid). - """ - try: - spec.validate_voluntary_exit_gossip(seen, state, signed_voluntary_exit) - return "valid", None - except spec.GossipIgnore as e: - return "ignore", str(e) - except spec.GossipReject as e: - return "reject", str(e) - - -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_voluntary_exit__valid(spec, state): """ @@ -73,14 +49,16 @@ def test_gossip_voluntary_exit__valid(spec, state): yield get_filename(signed_exit), signed_exit - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "valid" assert reason is None yield "messages", "meta", [{"message": get_filename(signed_exit), "expected": "valid"}] -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_voluntary_exit__ignore_already_seen(spec, state): """ @@ -104,13 +82,17 @@ def test_gossip_voluntary_exit__ignore_already_seen(spec, state): yield get_filename(signed_exit), signed_exit # First validation should pass - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "valid" assert reason is None messages.append({"message": get_filename(signed_exit), "expected": "valid"}) # Second validation should be ignored - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "ignore" assert reason == "already seen voluntary exit for this validator" messages.append({"message": get_filename(signed_exit), "expected": "ignore", "reason": reason}) @@ -118,7 +100,7 @@ def test_gossip_voluntary_exit__ignore_already_seen(spec, state): yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_voluntary_exit__reject_validator_index_out_of_range(spec, state): """ @@ -143,7 +125,9 @@ def test_gossip_voluntary_exit__reject_validator_index_out_of_range(spec, state) yield get_filename(signed_exit), signed_exit - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "reject" assert reason == "validator index out of range" @@ -154,7 +138,7 @@ def test_gossip_voluntary_exit__reject_validator_index_out_of_range(spec, state) ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_voluntary_exit__reject_validator_not_active(spec, state): """ @@ -177,7 +161,9 @@ def test_gossip_voluntary_exit__reject_validator_not_active(spec, state): yield get_filename(signed_exit), signed_exit - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "reject" assert reason == "validator is not active" @@ -188,7 +174,7 @@ def test_gossip_voluntary_exit__reject_validator_not_active(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_voluntary_exit__reject_already_initiated_exit(spec, state): """ @@ -211,7 +197,9 @@ def test_gossip_voluntary_exit__reject_already_initiated_exit(spec, state): yield get_filename(signed_exit), signed_exit - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "reject" assert reason == "validator has already initiated exit" @@ -222,7 +210,7 @@ def test_gossip_voluntary_exit__reject_already_initiated_exit(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_voluntary_exit__reject_epoch_in_future(spec, state): """ @@ -245,7 +233,9 @@ def test_gossip_voluntary_exit__reject_epoch_in_future(spec, state): yield get_filename(signed_exit), signed_exit - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "reject" assert reason == "voluntary exit epoch is in the future" @@ -256,7 +246,7 @@ def test_gossip_voluntary_exit__reject_epoch_in_future(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test def test_gossip_voluntary_exit__reject_not_active_long_enough(spec, state): """ @@ -280,7 +270,9 @@ def test_gossip_voluntary_exit__reject_not_active_long_enough(spec, state): yield get_filename(signed_exit), signed_exit - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "reject" assert reason == "validator has not been active long enough" @@ -291,7 +283,7 @@ def test_gossip_voluntary_exit__reject_not_active_long_enough(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA, FULU]) +@with_all_phases @spec_state_test @always_bls def test_gossip_voluntary_exit__reject_invalid_signature(spec, state): @@ -320,7 +312,9 @@ def test_gossip_voluntary_exit__reject_invalid_signature(spec, state): yield get_filename(signed_exit), signed_exit - result, reason = run_validate_voluntary_exit_gossip(spec, seen, state, signed_exit) + result, reason = run_validate_gossip( + spec, seen=seen, state=state, signed_voluntary_exit=signed_exit + ) assert result == "reject" assert reason == "invalid voluntary exit signature"