diff --git a/pysetup/spec_builders/capella.py b/pysetup/spec_builders/capella.py index e219ba3b940..d8cc8832cc7 100644 --- a/pysetup/spec_builders/capella.py +++ b/pysetup/spec_builders/capella.py @@ -28,8 +28,5 @@ def deprecate_functions(cls) -> set[str]: "process_historical_roots_update", "upgrade_to_bellatrix", "validate_merge_block", - # Temporarily deprecate gossip validation function until we add the capella version - # which does not use the deprecated is_execution_enabled function. - "validate_beacon_block_gossip", ] ) diff --git a/specs/capella/p2p-interface.md b/specs/capella/p2p-interface.md index d5a8608bb24..7c8218482b7 100644 --- a/specs/capella/p2p-interface.md +++ b/specs/capella/p2p-interface.md @@ -5,6 +5,7 @@ - [Introduction](#introduction) - [Modifications in Capella](#modifications-in-capella) - [Helpers](#helpers) + - [Modified `Seen`](#modified-seen) - [Modified `compute_fork_version`](#modified-compute_fork_version) - [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) - [Topics and messages](#topics-and-messages) @@ -31,6 +32,25 @@ specifications of previous upgrades, and assumes them as pre-requisite. ### Helpers +#### Modified `Seen` + +```python +@dataclass +class Seen(object): + proposer_slots: Set[Tuple[ValidatorIndex, Slot]] + aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + aggregate_data_roots: Dict[Root, Set[Tuple[boolean, ...]]] + voluntary_exit_indices: Set[ValidatorIndex] + proposer_slashing_indices: Set[ValidatorIndex] + attester_slashing_indices: Set[ValidatorIndex] + attestation_validator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + sync_contribution_aggregator_slots: Set[Tuple[ValidatorIndex, Slot, uint64]] + sync_contribution_data: Dict[Tuple[Slot, Root, uint64], Set[Tuple[boolean, ...]]] + sync_message_validator_slots: Set[Tuple[Slot, ValidatorIndex, uint64]] + # [New in Capella] + bls_to_execution_change_indices: Set[ValidatorIndex] +``` + #### Modified `compute_fork_version` ```python @@ -83,21 +103,161 @@ addition of `bls_to_execution_changes` to the inner `BeaconBlockBody`. See Capella [state transition document](./beacon-chain.md#beaconblockbody) for further details. -###### `bls_to_execution_change` +*Note*: `is_execution_enabled` is removed in Capella because execution is +unconditionally enabled after The Merge, which happens before Capella. -This topic is used to propagate signed bls to execution change messages to be -included in future blocks. +```python +def validate_beacon_block_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_beacon_block: SignedBeaconBlock, + current_time_ms: uint64, + block_payload_statuses: Dict[Root, PayloadValidationStatus] = {}, +) -> None: + """ + Validate a SignedBeaconBlock for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + block = signed_beacon_block.message + execution_payload = block.body.execution_payload + + # [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's execution payload timestamp is correct with respect to the slot + if execution_payload.timestamp != compute_time_at_slot(state, block.slot): + raise GossipReject("incorrect execution payload timestamp") + + parent_payload_status = PAYLOAD_STATUS_NOT_VALIDATED + if block.parent_root in block_payload_statuses: + parent_payload_status = block_payload_statuses[block.parent_root] + + if block.parent_root not in store.block_states: + if parent_payload_status == PAYLOAD_STATUS_NOT_VALIDATED: + # [REJECT] The block's parent passes validation + raise GossipReject("block's parent is invalid and EL result is unknown") + + # [IGNORE] The block's parent passes validation + raise GossipIgnore("block's parent is invalid and EL result is known") + + # [IGNORE] The block's parent's execution payload passes validation + if parent_payload_status == PAYLOAD_STATUS_INVALIDATED: + raise GossipIgnore("block's parent is valid and EL result is invalid") + + # [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 is proposed by the expected proposer for the slot + # (if shuffling is not available, IGNORE instead and MAY be queued for later) + 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_index does not match expected proposer") + + # Mark this block as seen for this proposer/slot combination + seen.proposer_slots.add((block.proposer_index, block.slot)) +``` + +###### `bls_to_execution_change` -The following validations MUST pass before forwarding the -`signed_bls_to_execution_change` on the network: +The `bls_to_execution_change` topic is used solely for propagating signed BLS to +execution change messages on the network. Signed messages are sent in their +entirety. The `state` parameter is the head state. -- _[IGNORE]_ `current_epoch >= CAPELLA_FORK_EPOCH`, where `current_epoch` is - defined by the current wall-clock time. -- _[IGNORE]_ The `signed_bls_to_execution_change` is the first valid signed bls - to execution change received for the validator with index - `signed_bls_to_execution_change.message.validator_index`. -- _[REJECT]_ All of the conditions within `process_bls_to_execution_change` pass - validation. +```python +def validate_bls_to_execution_change_gossip( + seen: Seen, + state: BeaconState, + signed_bls_to_execution_change: SignedBLSToExecutionChange, + current_time_ms: uint64, +) -> None: + """ + Validate a SignedBLSToExecutionChange for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + bls_to_execution_change = signed_bls_to_execution_change.message + validator_index = bls_to_execution_change.validator_index + + # [IGNORE] The current epoch is at or after the Capella fork epoch + # (where current_epoch is defined by the current wall-clock time) + time_since_genesis_ms = current_time_ms - state.genesis_time * 1000 + current_slot = Slot(time_since_genesis_ms // SLOT_DURATION_MS) + current_epoch = compute_epoch_at_slot(current_slot) + if current_epoch < CAPELLA_FORK_EPOCH: + raise GossipIgnore("current epoch is pre-capella") + + # [IGNORE] This is the first valid bls_to_execution_change received for the validator + if validator_index in seen.bls_to_execution_change_indices: + raise GossipIgnore("already seen BLS to execution change for this validator") + + # [REJECT] The validator index is valid + if validator_index >= len(state.validators): + raise GossipReject("validator index out of range") + + validator = state.validators[validator_index] + + # [REJECT] The validator has BLS withdrawal credentials + if validator.withdrawal_credentials[:1] != BLS_WITHDRAWAL_PREFIX: + raise GossipReject("validator does not have BLS withdrawal credentials") + + # [REJECT] The bls_to_execution_change is for the validator's withdrawal pubkey + if validator.withdrawal_credentials[1:] != hash(bls_to_execution_change.from_bls_pubkey)[1:]: + raise GossipReject("pubkey does not match validator withdrawal credentials") + + # [REJECT] The signature is valid + domain = compute_domain( + DOMAIN_BLS_TO_EXECUTION_CHANGE, genesis_validators_root=state.genesis_validators_root + ) + signing_root = compute_signing_root(bls_to_execution_change, domain) + if not bls.Verify( + bls_to_execution_change.from_bls_pubkey, + signing_root, + signed_bls_to_execution_change.signature, + ): + raise GossipReject("invalid BLS to execution change signature") + + # Mark this bls_to_execution_change as seen + seen.bls_to_execution_change_indices.add(validator_index) +``` #### Transitioning the gossip 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 61aa072ff5a..94f40f6ab5c 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 @@ -1,10 +1,10 @@ from eth_consensus_specs.test.context import ( always_bls, spec_state_test, - with_phases, + with_altair_and_later, with_presets, ) -from eth_consensus_specs.test.helpers.constants import ALTAIR, MAINNET +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.keys import privkeys from eth_consensus_specs.utils import bls @@ -137,7 +137,7 @@ def run_validate_contribution_gossip( return "reject", str(e) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test @always_bls def test_gossip_sync_committee_contribution_and_proof__valid(spec, state): @@ -181,7 +181,7 @@ def test_gossip_sync_committee_contribution_and_proof__valid(spec, state): ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test @always_bls def test_gossip_sync_committee_contribution_and_proof__valid_at_period_boundary(spec, state): @@ -231,7 +231,7 @@ def test_gossip_sync_committee_contribution_and_proof__valid_at_period_boundary( ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_contribution_and_proof__ignore_future_slot(spec, state): """Test that a contribution from a future slot is ignored.""" @@ -283,7 +283,7 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_future_slot(spec, ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_contribution_and_proof__ignore_past_slot(spec, state): """Test that a contribution from a past slot is ignored.""" @@ -339,7 +339,7 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_past_slot(spec, st ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_contribution_and_proof__reject_invalid_subcommittee_index( spec, state @@ -394,7 +394,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_invalid_subcommitt ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_contribution_and_proof__reject_no_participants(spec, state): """Test that a contribution with no participants is rejected.""" @@ -448,7 +448,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_no_participants(sp ) -@with_phases([ALTAIR]) +@with_altair_and_later @with_presets([MAINNET], reason="minimal preset has modulo=1, so everyone is an aggregator") @spec_state_test def test_gossip_sync_committee_contribution_and_proof__reject_not_aggregator(spec, state): @@ -519,7 +519,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_not_aggregator(spe ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_contribution_and_proof__reject_aggregator_not_in_subcommittee( spec, state @@ -577,7 +577,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_aggregator_not_in_ ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_contribution_and_proof__reject_aggregator_index_out_of_range( spec, state @@ -631,7 +631,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_aggregator_index_o ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test @always_bls def test_gossip_sync_committee_contribution_and_proof__ignore_superset_contribution(spec, state): @@ -755,7 +755,7 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_superset_contribut yield "messages", "meta", messages -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test @always_bls def test_gossip_sync_committee_contribution_and_proof__valid_non_superset_contribution(spec, state): @@ -874,7 +874,7 @@ def test_gossip_sync_committee_contribution_and_proof__valid_non_superset_contri yield "messages", "meta", messages -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test @always_bls def test_gossip_sync_committee_contribution_and_proof__ignore_duplicate_aggregator(spec, state): @@ -951,7 +951,7 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_duplicate_aggregat yield "messages", "meta", messages -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test @always_bls def test_gossip_sync_committee_contribution_and_proof__reject_invalid_selection_proof(spec, state): @@ -1012,7 +1012,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_invalid_selection_ ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test @always_bls def test_gossip_sync_committee_contribution_and_proof__reject_invalid_aggregator_signature( @@ -1072,7 +1072,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_invalid_aggregator ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test @always_bls def test_gossip_sync_committee_contribution_and_proof__reject_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 35c0b796fc8..93dc49a2422 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 @@ -1,9 +1,8 @@ from eth_consensus_specs.test.context import ( always_bls, spec_state_test, - with_phases, + with_altair_and_later, ) -from eth_consensus_specs.test.helpers.constants import ALTAIR from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.utils import bls @@ -57,7 +56,7 @@ def run_validate_sync_committee_message_gossip( return "reject", str(e) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test @always_bls def test_gossip_sync_committee_message__valid(spec, state): @@ -100,7 +99,7 @@ def test_gossip_sync_committee_message__valid(spec, state): ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_message__ignore_future_slot(spec, state): """Test that a sync committee message from a future slot is ignored.""" @@ -146,7 +145,7 @@ def test_gossip_sync_committee_message__ignore_future_slot(spec, state): ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_message__ignore_past_slot(spec, state): """Test that a sync committee message from a past slot is ignored.""" @@ -196,7 +195,7 @@ def test_gossip_sync_committee_message__ignore_past_slot(spec, state): ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_message__reject_wrong_subnet(spec, state): """Test that a sync committee message on the wrong subnet is rejected.""" @@ -242,7 +241,7 @@ def test_gossip_sync_committee_message__reject_wrong_subnet(spec, state): ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_message__reject_validator_index_out_of_range(spec, state): """Test that a sync committee message with validator index out of range is rejected.""" @@ -287,7 +286,7 @@ def test_gossip_sync_committee_message__reject_validator_index_out_of_range(spec ) -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test def test_gossip_sync_committee_message__ignore_duplicate(spec, state): """Test that a duplicate sync committee message is ignored.""" @@ -349,7 +348,7 @@ def test_gossip_sync_committee_message__ignore_duplicate(spec, state): yield "messages", "meta", messages -@with_phases([ALTAIR]) +@with_altair_and_later @spec_state_test @always_bls def test_gossip_sync_committee_message__reject_invalid_signature(spec, state): 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 f3e6142883e..f9352adf4c2 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 @@ -6,7 +6,7 @@ build_empty_block_for_next_slot, sign_block, ) -from eth_consensus_specs.test.helpers.constants import BELLATRIX +from eth_consensus_specs.test.helpers.constants import BELLATRIX, CAPELLA from eth_consensus_specs.test.helpers.execution_payload import ( build_empty_execution_payload, build_state_with_complete_transition, @@ -21,8 +21,8 @@ ) PAYLOAD_STATUS_VALID = "VALID" -PAYLOAD_STATUS_NOT_VALIDATED = "NOT_VALIDATED" PAYLOAD_STATUS_INVALIDATED = "INVALIDATED" +PAYLOAD_STATUS_NOT_VALIDATED = "NOT_VALIDATED" def wrap_genesis_block(spec, block): @@ -71,12 +71,7 @@ def run_validate_beacon_block_gossip( return "reject", str(e) -# ────────────────────────────────────────────────────────────────────────────── -# Execution payload timestamp checks (new in Bellatrix) -# ────────────────────────────────────────────────────────────────────────────── - - -@with_phases([BELLATRIX]) +@with_phases([BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__valid_execution_enabled(spec, state): """ @@ -157,7 +152,7 @@ def test_gossip_beacon_block__valid_execution_disabled(spec, state): ) -@with_phases([BELLATRIX]) +@with_phases([BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__reject_incorrect_execution_payload_timestamp(spec, state): """ @@ -208,12 +203,7 @@ def test_gossip_beacon_block__reject_incorrect_execution_payload_timestamp(spec, ) -# ────────────────────────────────────────────────────────────────────────────── -# Execution payload verification status (parent checks, new in Bellatrix) -# ────────────────────────────────────────────────────────────────────────────── - - -@with_phases([BELLATRIX]) +@with_phases([BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__reject_parent_consensus_failed_execution_not_verified(spec, state): """ @@ -302,7 +292,7 @@ def test_gossip_beacon_block__reject_parent_consensus_failed_execution_not_verif ) -@with_phases([BELLATRIX]) +@with_phases([BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__ignore_parent_consensus_failed_execution_known(spec, state): """ @@ -390,7 +380,7 @@ def test_gossip_beacon_block__ignore_parent_consensus_failed_execution_known(spe ) -@with_phases([BELLATRIX]) +@with_phases([BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__ignore_parent_execution_verified_invalid(spec, state): """ @@ -480,7 +470,7 @@ def test_gossip_beacon_block__ignore_parent_execution_verified_invalid(spec, sta ) -@with_phases([BELLATRIX]) +@with_phases([BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__valid_parent_execution_verified_valid(spec, state): """ @@ -562,7 +552,7 @@ def test_gossip_beacon_block__valid_parent_execution_verified_valid(spec, state) ) -@with_phases([BELLATRIX]) +@with_phases([BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__valid_parent_optimistic(spec, state): """ @@ -642,251 +632,3 @@ def test_gossip_beacon_block__valid_parent_optimistic(spec, state): "meta", [{"offset_ms": 500, "message": get_filename(signed_child), "expected": "valid"}], ) - - -# ────────────────────────────────────────────────────────────────────────────── -# Inherited Phase 0 checks in Bellatrix-specific contexts -# (These tests cover Bellatrix behavior that is distinct from the Phase 0 -# version of the same validation.) -# ────────────────────────────────────────────────────────────────────────────── - - -@with_phases([BELLATRIX]) -@spec_state_test -def test_gossip_beacon_block__reject_parent_failed_validation(spec, state): - """ - Test that when execution is disabled, a block whose parent failed validation - is rejected. - """ - yield "topic", "meta", "beacon_block" - - state = build_state_with_incomplete_transition(spec, state) - yield "state", state - - seen = get_seen(spec) - store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) - signed_anchor = wrap_genesis_block(spec, anchor_block) - - yield get_filename(signed_anchor), signed_anchor - - block = build_empty_block_for_next_slot(spec, state) - block.body.execution_payload = spec.ExecutionPayload() - signed_block = state_transition_and_sign_block(spec, state, block) - - yield get_filename(signed_block), signed_block - - # Add block to store.blocks but NOT to store.block_states (simulating failed validation) - store.blocks[signed_block.message.hash_tree_root()] = signed_block.message - - yield ( - "blocks", - "meta", - [ - {"block": get_filename(signed_anchor)}, - {"block": get_filename(signed_block), "failed": True}, - ], - ) - - # Build child block with execution disabled - child_slot = signed_block.message.slot + 1 - temp_state = state.copy() - spec.process_slots(temp_state, child_slot) - proposer_index = spec.get_beacon_proposer_index(temp_state) - - child_block = spec.BeaconBlock( - slot=child_slot, - proposer_index=proposer_index, - parent_root=signed_block.message.hash_tree_root(), - state_root=temp_state.hash_tree_root(), - ) - child_block.body.execution_payload = spec.ExecutionPayload() - signed_child = sign_block(spec, temp_state, child_block, proposer_index=proposer_index) - - yield get_filename(signed_child), signed_child - - block_time_ms = spec.compute_time_at_slot_ms(state, child_block.slot) - - 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 - ) - assert result == "reject" - assert reason == "block's parent is invalid and execution is not enabled" - - yield ( - "messages", - "meta", - [ - { - "offset_ms": 500, - "message": get_filename(signed_child), - "expected": "reject", - "reason": reason, - } - ], - ) - - -@with_phases([BELLATRIX]) -@spec_state_test -def test_gossip_beacon_block__reject_slot_not_higher_than_parent(spec, state): - """ - Test that a block with slot <= parent slot is rejected. - Uses execution-enabled block with proper execution payload. - """ - yield "topic", "meta", "beacon_block" - - state = build_state_with_complete_transition(spec, state) - yield "state", state - - seen = get_seen(spec) - store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) - signed_anchor = wrap_genesis_block(spec, anchor_block) - - yield get_filename(signed_anchor), signed_anchor - - # Build and add a valid block at slot 1 - parent_block = build_empty_block_for_next_slot(spec, state) - signed_parent = state_transition_and_sign_block(spec, state, parent_block) - - yield get_filename(signed_parent), signed_parent - - # Add parent to store - store.blocks[signed_parent.message.hash_tree_root()] = signed_parent.message - store.block_states[signed_parent.message.hash_tree_root()] = state.copy() - - yield ( - "blocks", - "meta", - [ - {"block": get_filename(signed_anchor)}, - {"block": get_filename(signed_parent)}, - ], - ) - - # Build a block at the same slot as parent (invalid) - proposer_index = signed_parent.message.proposer_index - - block = spec.BeaconBlock( - slot=signed_parent.message.slot, # Same slot as parent - invalid! - proposer_index=proposer_index, - parent_root=signed_parent.message.hash_tree_root(), - state_root=state.hash_tree_root(), - ) - # Need to add execution payload with correct timestamp for this slot - # Use a temp state at the same slot as the block - temp_state = state.copy() - block.body.execution_payload = build_empty_execution_payload(spec, temp_state) - signed_block = sign_block(spec, state, block, proposer_index=proposer_index) - - yield get_filename(signed_block), signed_block - - 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 - ) - assert result == "reject" - assert reason == "block is not from a higher slot than its parent" - - yield ( - "messages", - "meta", - [ - { - "offset_ms": 500, - "message": get_filename(signed_block), - "expected": "reject", - "reason": reason, - } - ], - ) - - -@with_phases([BELLATRIX]) -@spec_state_test -def test_gossip_beacon_block__reject_finalized_checkpoint_not_ancestor(spec, state): - """ - Test that a block whose finalized checkpoint is not an ancestor is rejected. - Uses execution-enabled block with proper execution payload. - """ - yield "topic", "meta", "beacon_block" - - state = build_state_with_complete_transition(spec, state) - yield "state", state - - seen = get_seen(spec) - store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) - signed_anchor = wrap_genesis_block(spec, anchor_block) - - yield get_filename(signed_anchor), signed_anchor - - block = build_empty_block_for_next_slot(spec, state) - signed_block = state_transition_and_sign_block(spec, state, block) - - yield get_filename(signed_block), signed_block - - # Add block to store - store.blocks[signed_block.message.hash_tree_root()] = signed_block.message - store.block_states[signed_block.message.hash_tree_root()] = state.copy() - - yield ( - "blocks", - "meta", - [ - {"block": get_filename(signed_anchor)}, - {"block": get_filename(signed_block)}, - ], - ) - - # Set finalized checkpoint to something that is NOT an ancestor - fake_finalized_root = spec.Root(b"\xab" * 32) - store.finalized_checkpoint = spec.Checkpoint( - epoch=spec.Epoch(0), - root=fake_finalized_root, - ) - - yield "finalized_checkpoint", "meta", {"epoch": 0, "root": "0x" + "ab" * 32} - - # Build child block with proper execution payload - child_slot = signed_block.message.slot + 1 - temp_state = state.copy() - spec.process_slots(temp_state, child_slot) - proposer_index = spec.get_beacon_proposer_index(temp_state) - - child_block = spec.BeaconBlock( - slot=child_slot, - proposer_index=proposer_index, - parent_root=signed_block.message.hash_tree_root(), - state_root=temp_state.hash_tree_root(), - ) - child_block.body.execution_payload = build_empty_execution_payload(spec, temp_state) - signed_child = sign_block(spec, temp_state, child_block, proposer_index=proposer_index) - - yield get_filename(signed_child), signed_child - - block_time_ms = spec.compute_time_at_slot_ms(state, child_block.slot) - - 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 - ) - assert result == "reject" - assert reason == "finalized checkpoint is not an ancestor of block" - - yield ( - "messages", - "meta", - [ - { - "offset_ms": 500, - "message": get_filename(signed_child), - "expected": "reject", - "reason": reason, - } - ], - ) diff --git a/tests/core/pyspec/eth_consensus_specs/test/capella/networking/__init__.py b/tests/core/pyspec/eth_consensus_specs/test/capella/networking/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/capella/networking/__init__.py @@ -0,0 +1 @@ + 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 new file mode 100644 index 00000000000..3d13b659fe3 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/capella/networking/test_gossip_bls_to_execution_change.py @@ -0,0 +1,314 @@ +from eth_consensus_specs.test.context import ( + always_bls, + spec_configured_state_test, + with_capella_and_later, +) +from eth_consensus_specs.test.helpers.bls_to_execution_changes import ( + get_signed_address_change as get_signed_bls_to_execution_change, +) +from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen +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. + """ + capella_slot = spec.compute_start_slot_at_epoch(spec.config.CAPELLA_FORK_EPOCH) + return spec.compute_time_at_slot_ms(state, capella_slot) + + +@with_capella_and_later +@spec_configured_state_test({"CAPELLA_FORK_EPOCH": 0}) +def test_gossip_bls_to_execution_change__valid(spec, state): + """ + Test that a valid `bls_to_execution_change` passes gossip validation. + """ + yield "topic", "meta", "bls_to_execution_change" + yield "state", state + + seen = get_seen(spec) + signed_bls_to_execution_change = get_signed_bls_to_execution_change(spec, state) + current_time_ms = get_capella_fork_time_ms(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 + ) + assert result == "valid" + assert reason is None + + yield ( + "messages", + "meta", + [ + { + "offset_ms": 0, + "message": get_filename(signed_bls_to_execution_change), + "expected": "valid", + } + ], + ) + + +@with_capella_and_later +@spec_configured_state_test({"CAPELLA_FORK_EPOCH": 1}) +def test_gossip_bls_to_execution_change__ignore_pre_capella(spec, state): + """ + Test that a `bls_to_execution_change` before the Capella fork is ignored. + """ + yield "topic", "meta", "bls_to_execution_change" + yield "state", state + + seen = get_seen(spec) + signed_bls_to_execution_change = get_signed_bls_to_execution_change(spec, state) + current_time_ms = spec.compute_time_at_slot_ms(state, spec.Slot(0)) + + 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 + ) + assert result == "ignore" + assert reason == "current epoch is pre-capella" + + yield ( + "messages", + "meta", + [ + { + "offset_ms": 0, + "message": get_filename(signed_bls_to_execution_change), + "expected": "ignore", + "reason": reason, + } + ], + ) + + +@with_capella_and_later +@spec_configured_state_test({"CAPELLA_FORK_EPOCH": 0}) +def test_gossip_bls_to_execution_change__ignore_already_seen(spec, state): + """ + Test that a duplicate `bls_to_execution_change` is ignored. + """ + yield "topic", "meta", "bls_to_execution_change" + yield "state", state + + messages = [] + seen = get_seen(spec) + signed_bls_to_execution_change = get_signed_bls_to_execution_change(spec, state) + current_time_ms = get_capella_fork_time_ms(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 + ) + assert result == "valid" + assert reason is None + messages.append( + { + "offset_ms": 0, + "message": get_filename(signed_bls_to_execution_change), + "expected": "valid", + } + ) + + result, reason = run_validate_bls_to_execution_change_gossip( + spec, seen, state, signed_bls_to_execution_change, current_time_ms + ) + assert result == "ignore" + assert reason == "already seen BLS to execution change for this validator" + messages.append( + { + "offset_ms": 0, + "message": get_filename(signed_bls_to_execution_change), + "expected": "ignore", + "reason": reason, + } + ) + + yield "messages", "meta", messages + + +@with_capella_and_later +@spec_configured_state_test({"CAPELLA_FORK_EPOCH": 0}) +def test_gossip_bls_to_execution_change__reject_validator_index_out_of_range(spec, state): + """ + Test that a `bls_to_execution_change` with validator index out of range is rejected. + """ + yield "topic", "meta", "bls_to_execution_change" + yield "state", state + + seen = get_seen(spec) + signed_bls_to_execution_change = get_signed_bls_to_execution_change( + spec, state, validator_index=len(state.validators) + ) + current_time_ms = get_capella_fork_time_ms(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 + ) + assert result == "reject" + assert reason == "validator index out of range" + + yield ( + "messages", + "meta", + [ + { + "offset_ms": 0, + "message": get_filename(signed_bls_to_execution_change), + "expected": "reject", + "reason": reason, + } + ], + ) + + +@with_capella_and_later +@spec_configured_state_test({"CAPELLA_FORK_EPOCH": 0}) +def test_gossip_bls_to_execution_change__reject_not_bls_credentials(spec, state): + """ + Test that a `bls_to_execution_change` for a validator without BLS credentials is rejected. + """ + yield "topic", "meta", "bls_to_execution_change" + + seen = get_seen(spec) + validator_index = len(state.validators) // 2 + state.validators[validator_index].withdrawal_credentials = b"\x01" + b"\x00" * 11 + b"\x23" * 20 + yield "state", state + + signed_bls_to_execution_change = get_signed_bls_to_execution_change( + spec, state, validator_index=validator_index + ) + current_time_ms = get_capella_fork_time_ms(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 + ) + assert result == "reject" + assert reason == "validator does not have BLS withdrawal credentials" + + yield ( + "messages", + "meta", + [ + { + "offset_ms": 0, + "message": get_filename(signed_bls_to_execution_change), + "expected": "reject", + "reason": reason, + } + ], + ) + + +@with_capella_and_later +@spec_configured_state_test({"CAPELLA_FORK_EPOCH": 0}) +def test_gossip_bls_to_execution_change__reject_pubkey_mismatch(spec, state): + """ + Test that a `bls_to_execution_change` with the wrong withdrawal pubkey is rejected. + """ + yield "topic", "meta", "bls_to_execution_change" + yield "state", state + + seen = get_seen(spec) + validator_index = 2 + signed_bls_to_execution_change = get_signed_bls_to_execution_change( + spec, + state, + validator_index=validator_index, + withdrawal_pubkey=pubkeys[0], + ) + current_time_ms = get_capella_fork_time_ms(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 + ) + assert result == "reject" + assert reason == "pubkey does not match validator withdrawal credentials" + + yield ( + "messages", + "meta", + [ + { + "offset_ms": 0, + "message": get_filename(signed_bls_to_execution_change), + "expected": "reject", + "reason": reason, + } + ], + ) + + +@with_capella_and_later +@spec_configured_state_test({"CAPELLA_FORK_EPOCH": 0}) +@always_bls +def test_gossip_bls_to_execution_change__reject_bad_signature(spec, state): + """ + Test that a `bls_to_execution_change` with an invalid signature is rejected. + """ + yield "topic", "meta", "bls_to_execution_change" + yield "state", state + + seen = get_seen(spec) + signed_bls_to_execution_change = get_signed_bls_to_execution_change(spec, state) + signed_bls_to_execution_change.signature = spec.BLSSignature(b"\x42" * 96) + current_time_ms = get_capella_fork_time_ms(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 + ) + assert result == "reject" + assert reason == "invalid BLS to execution change signature" + + yield ( + "messages", + "meta", + [ + { + "offset_ms": 0, + "message": get_filename(signed_bls_to_execution_change), + "expected": "reject", + "reason": reason, + } + ], + ) 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 25fdd28a4f4..ee318a84d90 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/gossip.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/gossip.py @@ -1,6 +1,6 @@ from eth_utils import encode_hex -from eth_consensus_specs.test.helpers.forks import is_post_altair +from eth_consensus_specs.test.helpers.forks import is_post_altair, is_post_capella def get_seen(spec): @@ -22,6 +22,12 @@ def get_seen(spec): sync_message_validator_slots=set(), ) ) + if is_post_capella(spec): + kwargs.update( + dict( + bls_to_execution_change_indices=set(), + ) + ) return spec.Seen(**kwargs) @@ -47,6 +53,9 @@ def get_filename(obj): prefix = "contribution" elif class_name == "SyncCommitteeMessage": prefix = "sync_committee_message" + # capella + elif "BLSToExecutionChange" in class_name: + prefix = "bls_to_execution_change" else: raise Exception(f"unsupported type: {class_name}") 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 0b18a678939..6276540a331 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 @@ -15,7 +15,7 @@ from eth_consensus_specs.test.helpers.block import ( build_empty_block_for_next_slot, ) -from eth_consensus_specs.test.helpers.constants import ALTAIR, BELLATRIX, MAINNET, PHASE0 +from eth_consensus_specs.test.helpers.constants import ALTAIR, BELLATRIX, CAPELLA, MAINNET, PHASE0 from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) @@ -91,7 +91,7 @@ def run_validate_beacon_aggregate_and_proof_gossip( return "reject", str(e) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__valid(spec, state): """ @@ -132,7 +132,7 @@ def test_gossip_beacon_aggregate_and_proof__valid(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_committee_index_out_of_range(spec, state): """ @@ -184,7 +184,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_committee_index_out_of_range( ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_slot_not_within_range(spec, state): """ @@ -234,7 +234,7 @@ def test_gossip_beacon_aggregate_and_proof__ignore_slot_not_within_range(spec, s ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__valid_within_clock_disparity(spec, state): """ @@ -283,7 +283,7 @@ def test_gossip_beacon_aggregate_and_proof__valid_within_clock_disparity(spec, s ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_epoch_mismatch(spec, state): """ @@ -334,7 +334,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_epoch_mismatch(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregate(spec, state): """ @@ -388,7 +388,7 @@ def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregate(spec, yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_same_data_root_without_superset(spec, state): """ @@ -477,7 +477,7 @@ def test_gossip_beacon_aggregate_and_proof__ignore_same_data_root_without_supers yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__valid_two_aggregators_same_data(spec, state): """ @@ -561,7 +561,7 @@ def test_gossip_beacon_aggregate_and_proof__valid_two_aggregators_same_data(spec yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_block_not_seen(spec, state): """ @@ -615,7 +615,7 @@ def test_gossip_beacon_aggregate_and_proof__ignore_block_not_seen(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_aggregation_bits_size_mismatch(spec, state): """ @@ -672,7 +672,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_aggregation_bits_size_mismatc ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_no_participants(spec, state): """ @@ -727,7 +727,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_no_participants(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregator(spec, state): """ @@ -790,7 +790,7 @@ def test_gossip_beacon_aggregate_and_proof__ignore_already_seen_aggregator(spec, yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @with_presets([MAINNET], reason="minimal preset has committees < 16, so everyone is an aggregator") @spec_test @with_custom_state( @@ -879,7 +879,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_not_aggregator(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_aggregator_not_in_committee(spec, state): """ @@ -938,7 +938,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_aggregator_not_in_committee(s ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_aggregator_index_out_of_range(spec, state): """ @@ -988,7 +988,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_aggregator_index_out_of_range ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test @always_bls def test_gossip_beacon_aggregate_and_proof__reject_invalid_selection_proof(spec, state): @@ -1040,7 +1040,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_invalid_selection_proof(spec, ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test @always_bls def test_gossip_beacon_aggregate_and_proof__reject_invalid_aggregator_signature(spec, state): @@ -1092,7 +1092,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_invalid_aggregator_signature( ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test @always_bls def test_gossip_beacon_aggregate_and_proof__reject_invalid_aggregate_signature(spec, state): @@ -1144,7 +1144,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_invalid_aggregate_signature(s ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_block_failed_validation(spec, state): """ @@ -1208,7 +1208,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_block_failed_validation(spec, ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__reject_target_not_ancestor(spec, state): """ @@ -1260,7 +1260,7 @@ def test_gossip_beacon_aggregate_and_proof__reject_target_not_ancestor(spec, sta ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_aggregate_and_proof__ignore_finalized_not_ancestor(spec, state): """ 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 4bbad50502d..0968d0d9dc2 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 @@ -9,7 +9,7 @@ from eth_consensus_specs.test.helpers.block import ( build_empty_block_for_next_slot, ) -from eth_consensus_specs.test.helpers.constants import ALTAIR, BELLATRIX, PHASE0 +from eth_consensus_specs.test.helpers.constants import ALTAIR, BELLATRIX, CAPELLA, PHASE0 from eth_consensus_specs.test.helpers.fork_choice import ( get_genesis_forkchoice_store_and_block, ) @@ -53,7 +53,7 @@ def run_validate_beacon_attestation_gossip( return "reject", str(e) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__valid(spec, state): """ @@ -113,7 +113,7 @@ def test_gossip_beacon_attestation__valid(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__reject_committee_index_out_of_range(spec, state): """ @@ -166,7 +166,7 @@ def test_gossip_beacon_attestation__reject_committee_index_out_of_range(spec, st ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__reject_wrong_subnet(spec, state): """ @@ -217,7 +217,7 @@ def test_gossip_beacon_attestation__reject_wrong_subnet(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__ignore_slot_not_in_range(spec, state): """ @@ -278,7 +278,7 @@ def test_gossip_beacon_attestation__ignore_slot_not_in_range(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__valid_within_clock_disparity(spec, state): """ @@ -338,7 +338,7 @@ def test_gossip_beacon_attestation__valid_within_clock_disparity(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__valid_within_clock_disparity_old(spec, state): """ @@ -400,7 +400,7 @@ def test_gossip_beacon_attestation__valid_within_clock_disparity_old(spec, state ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__ignore_slot_too_old(spec, state): """ @@ -463,7 +463,7 @@ def test_gossip_beacon_attestation__ignore_slot_too_old(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__reject_epoch_mismatch(spec, state): """ @@ -515,7 +515,7 @@ def test_gossip_beacon_attestation__reject_epoch_mismatch(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__reject_not_unaggregated(spec, state): """ @@ -573,7 +573,7 @@ def test_gossip_beacon_attestation__reject_not_unaggregated(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__reject_aggregation_bits_size_mismatch(spec, state): """ @@ -629,7 +629,7 @@ def test_gossip_beacon_attestation__reject_aggregation_bits_size_mismatch(spec, ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__ignore_already_seen(spec, state): """ @@ -702,7 +702,7 @@ def test_gossip_beacon_attestation__ignore_already_seen(spec, state): yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__ignore_block_not_seen(spec, state): """ @@ -764,7 +764,7 @@ def test_gossip_beacon_attestation__ignore_block_not_seen(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__reject_block_failed_validation(spec, state): """ @@ -839,7 +839,7 @@ def test_gossip_beacon_attestation__reject_block_failed_validation(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test @always_bls def test_gossip_beacon_attestation__reject_invalid_signature(spec, state): @@ -898,7 +898,7 @@ def test_gossip_beacon_attestation__reject_invalid_signature(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__reject_target_not_ancestor(spec, state): """ @@ -959,7 +959,7 @@ def test_gossip_beacon_attestation__reject_target_not_ancestor(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_attestation__ignore_finalized_not_ancestor(spec, state): """ 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 233fdffb146..f5be4cd95bf 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 @@ -7,10 +7,15 @@ build_empty_block_for_next_slot, sign_block, ) -from eth_consensus_specs.test.helpers.constants import ALTAIR, BELLATRIX, PHASE0 +from eth_consensus_specs.test.helpers.constants import ALTAIR, BELLATRIX, CAPELLA, PHASE0 +from eth_consensus_specs.test.helpers.execution_payload import ( + build_empty_execution_payload, + build_state_with_incomplete_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_bellatrix from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen from eth_consensus_specs.test.helpers.state import ( state_transition_and_sign_block, @@ -37,7 +42,7 @@ def run_validate_beacon_block_gossip(spec, seen, store, state, signed_block, cur return "reject", str(e) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__valid_block(spec, state): """ @@ -75,7 +80,7 @@ def test_gossip_beacon_block__valid_block(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__ignore_future_slot(spec, state): """ @@ -121,7 +126,7 @@ def test_gossip_beacon_block__ignore_future_slot(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__valid_within_clock_disparity(spec, state): """ @@ -160,7 +165,7 @@ def test_gossip_beacon_block__valid_within_clock_disparity(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__ignore_already_seen_proposer_slot(spec, state): """ @@ -212,7 +217,7 @@ def test_gossip_beacon_block__ignore_already_seen_proposer_slot(spec, state): yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__ignore_slot_not_greater_than_finalized(spec, state): """ @@ -284,7 +289,7 @@ def test_gossip_beacon_block__ignore_slot_not_greater_than_finalized(spec, state ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__ignore_parent_not_seen(spec, state): """ @@ -339,15 +344,22 @@ def test_gossip_beacon_block__ignore_parent_not_seen(spec, state): ) -@with_phases([PHASE0, ALTAIR]) +@with_phases([PHASE0, ALTAIR, BELLATRIX]) @spec_state_test def test_gossip_beacon_block__reject_parent_failed_validation(spec, state): """ Test that a block whose parent failed validation is rejected. This happens when parent is in store.blocks but not in store.block_states. + + On Bellatrix, execution is disabled (pre-merge state) so we exercise the + else-branch of is_execution_enabled. Capella is excluded because it has no + pre-merge state. """ yield "topic", "meta", "beacon_block" + if is_post_bellatrix(spec): + state = build_state_with_incomplete_transition(spec, state) + seen = get_seen(spec) store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) signed_anchor = wrap_genesis_block(spec, anchor_block) @@ -355,6 +367,8 @@ def test_gossip_beacon_block__reject_parent_failed_validation(spec, state): yield get_filename(signed_anchor), signed_anchor block = build_empty_block_for_next_slot(spec, state) + if is_post_bellatrix(spec): + block.body.execution_payload = spec.ExecutionPayload() signed_block = state_transition_and_sign_block(spec, state, block) yield "state", state @@ -385,6 +399,8 @@ def test_gossip_beacon_block__reject_parent_failed_validation(spec, state): parent_root=signed_block.message.hash_tree_root(), state_root=temp_state.hash_tree_root(), ) + if is_post_bellatrix(spec): + child_block.body.execution_payload = spec.ExecutionPayload() signed_child = sign_block(spec, temp_state, child_block, proposer_index=proposer_index) yield get_filename(signed_child), signed_child @@ -397,7 +413,10 @@ def test_gossip_beacon_block__reject_parent_failed_validation(spec, state): spec, seen, store, state, signed_child, block_time_ms + 500 ) assert result == "reject" - assert reason == "block's parent is invalid" + if is_post_bellatrix(spec): + assert reason == "block's parent is invalid and execution is not enabled" + else: + assert reason == "block's parent is invalid" yield ( "messages", @@ -413,7 +432,7 @@ def test_gossip_beacon_block__reject_parent_failed_validation(spec, state): ) -@with_phases([PHASE0, ALTAIR]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__reject_slot_not_higher_than_parent(spec, state): """ @@ -457,6 +476,8 @@ def test_gossip_beacon_block__reject_slot_not_higher_than_parent(spec, state): parent_root=signed_parent.message.hash_tree_root(), state_root=state.hash_tree_root(), ) + if is_post_bellatrix(spec): + block.body.execution_payload = build_empty_execution_payload(spec, state) signed_block = sign_block(spec, state, block, proposer_index=proposer_index) yield get_filename(signed_block), signed_block @@ -485,7 +506,7 @@ def test_gossip_beacon_block__reject_slot_not_higher_than_parent(spec, state): ) -@with_phases([PHASE0, ALTAIR]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__reject_finalized_checkpoint_not_ancestor(spec, state): """ @@ -540,6 +561,8 @@ def test_gossip_beacon_block__reject_finalized_checkpoint_not_ancestor(spec, sta parent_root=signed_block.message.hash_tree_root(), state_root=temp_state.hash_tree_root(), ) + if is_post_bellatrix(spec): + child_block.body.execution_payload = build_empty_execution_payload(spec, temp_state) signed_child = sign_block(spec, temp_state, child_block, proposer_index=proposer_index) yield get_filename(signed_child), signed_child @@ -568,7 +591,7 @@ def test_gossip_beacon_block__reject_finalized_checkpoint_not_ancestor(spec, sta ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test @always_bls def test_gossip_beacon_block__reject_invalid_proposer_signature(spec, state): @@ -617,7 +640,7 @@ def test_gossip_beacon_block__reject_invalid_proposer_signature(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__reject_invalid_proposer_index(spec, state): """ @@ -665,7 +688,7 @@ def test_gossip_beacon_block__reject_invalid_proposer_index(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_beacon_block__reject_wrong_proposer_index(spec, state): """ 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 f0ea9ff49ef..649c4dff57e 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 @@ -3,7 +3,7 @@ spec_state_test, with_phases, ) -from eth_consensus_specs.test.helpers.constants import ALTAIR, BELLATRIX, PHASE0 +from eth_consensus_specs.test.helpers.constants import ALTAIR, BELLATRIX, CAPELLA, PHASE0 from eth_consensus_specs.test.helpers.gossip import get_filename, get_seen from eth_consensus_specs.test.helpers.keys import privkeys from eth_consensus_specs.test.helpers.state import ( @@ -43,7 +43,7 @@ def run_validate_voluntary_exit_gossip(spec, seen, state, signed_voluntary_exit) return "reject", str(e) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_voluntary_exit__valid(spec, state): """ @@ -72,7 +72,7 @@ def test_gossip_voluntary_exit__valid(spec, state): yield "messages", "meta", [{"message": get_filename(signed_exit), "expected": "valid"}] -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_voluntary_exit__ignore_already_seen(spec, state): """ @@ -110,7 +110,7 @@ def test_gossip_voluntary_exit__ignore_already_seen(spec, state): yield "messages", "meta", messages -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_voluntary_exit__reject_validator_index_out_of_range(spec, state): """ @@ -146,7 +146,7 @@ def test_gossip_voluntary_exit__reject_validator_index_out_of_range(spec, state) ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_voluntary_exit__reject_validator_not_active(spec, state): """ @@ -180,7 +180,7 @@ def test_gossip_voluntary_exit__reject_validator_not_active(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_voluntary_exit__reject_already_initiated_exit(spec, state): """ @@ -214,7 +214,7 @@ def test_gossip_voluntary_exit__reject_already_initiated_exit(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_voluntary_exit__reject_epoch_in_future(spec, state): """ @@ -248,7 +248,7 @@ def test_gossip_voluntary_exit__reject_epoch_in_future(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test def test_gossip_voluntary_exit__reject_not_active_long_enough(spec, state): """ @@ -283,7 +283,7 @@ def test_gossip_voluntary_exit__reject_not_active_long_enough(spec, state): ) -@with_phases([PHASE0, ALTAIR, BELLATRIX]) +@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA]) @spec_state_test @always_bls def test_gossip_voluntary_exit__reject_invalid_signature(spec, state): diff --git a/tests/formats/networking/gossip_validation.md b/tests/formats/networking/gossip_validation.md index e43cc7049c3..4c3d02596d5 100644 --- a/tests/formats/networking/gossip_validation.md +++ b/tests/formats/networking/gossip_validation.md @@ -6,7 +6,6 @@ validation rules for messages received via gossip topics. - [Test case format](#test-case-format) - - [Directory structure](#directory-structure) - [`meta.yaml`](#metayaml) - [`state.ssz_snappy`](#statessz_snappy) - [Message files](#message-files) @@ -17,17 +16,6 @@ validation rules for messages received via gossip topics. ## Test case format -### Directory structure - -``` -tests/gossip/// -├── meta.yaml -├── state.ssz_snappy -├── block_<32-byte-root>.ssz_snappy # block file(s) -├── attestation_<32-byte-root>.ssz_snappy # attestation file(s) (attestation topics) -└── aggregate_<32-byte-root>.ssz_snappy # aggregate file(s) (aggregate topic) -``` - ### `meta.yaml` ```yaml @@ -67,16 +55,17 @@ An SSZ-snappy encoded `BeaconState`. This state provides: Message files are named with a prefix indicating their type and the 32-byte hash tree root: -| Topic | File prefix | SSZ type | -| --------------------------------------- | ------------------------- | ---------------------------- | -| `beacon_block` | `block_` | `SignedBeaconBlock` | -| `beacon_attestation` | `attestation_` | `Attestation` | -| `beacon_aggregate_and_proof` | `aggregate_` | `SignedAggregateAndProof` | -| `proposer_slashing` | `proposer_slashing_` | `ProposerSlashing` | -| `attester_slashing` | `attester_slashing_` | `AttesterSlashing` | -| `voluntary_exit` | `voluntary_exit_` | `SignedVoluntaryExit` | -| `sync_committee_contribution_and_proof` | `contribution_` | `SignedContributionAndProof` | -| `sync_committee` | `sync_committee_message_` | `SyncCommitteeMessage` | +| Topic | File prefix | SSZ type | +| --------------------------------------- | -------------------------- | ---------------------------- | +| `beacon_block` | `block_` | `SignedBeaconBlock` | +| `beacon_attestation` | `attestation_` | `Attestation` | +| `beacon_aggregate_and_proof` | `aggregate_` | `SignedAggregateAndProof` | +| `proposer_slashing` | `proposer_slashing_` | `ProposerSlashing` | +| `attester_slashing` | `attester_slashing_` | `AttesterSlashing` | +| `voluntary_exit` | `voluntary_exit_` | `SignedVoluntaryExit` | +| `sync_committee_contribution_and_proof` | `contribution_` | `SignedContributionAndProof` | +| `sync_committee` | `sync_committee_message_` | `SyncCommitteeMessage` | +| `bls_to_execution_change` | `bls_to_execution_change_` | `SignedBLSToExecutionChange` | Block files (`block_.ssz_snappy`) serve multiple purposes: