Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions pysetup/spec_builders/capella.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
)
186 changes: 174 additions & 12 deletions specs/capella/p2p-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -83,21 +103,163 @@ 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`
The *type* of the payload of this topic changes to the (modified)
`SignedBeaconBlock` found in Capella. Specifically, this type changes with the
addition of `bls_to_execution_changes` to the inner `BeaconBlockBody`. See
Capella [state transition document](./beacon-chain.md#beaconblockbody) for
further details.
Comment thread
jtraglia marked this conversation as resolved.
Outdated

This topic is used to propagate signed bls to execution change messages to be
included in future blocks.
*Note*: `is_execution_enabled` is removed in Capella because execution is
unconditionally enabled after The Merge, which happens before Capella.

The following validations MUST pass before forwarding the
`signed_bls_to_execution_change` on the network:
```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
Comment thread
jihoonsong marked this conversation as resolved.
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)
Comment thread
jihoonsong marked this conversation as resolved.
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
Comment thread
jihoonsong marked this conversation as resolved.
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`

- _[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.
The `bls_to_execution_change` topic is used solely for propagating signed BLS to
execution change (BTEC) messages on the network. Signed BTEC messages are sent
Comment thread
jihoonsong marked this conversation as resolved.
Outdated
in their entirety. The `state` parameter is the head state.

```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.
"""
btec = signed_bls_to_execution_change.message
Comment thread
jihoonsong marked this conversation as resolved.
Outdated
validator_index = btec.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 BTEC 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 BTEC is for the validator's withdrawal pubkey
if validator.withdrawal_credentials[1:] != hash(btec.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(btec, domain)
if not bls.Verify(
btec.from_bls_pubkey,
signing_root,
signed_bls_to_execution_change.signature,
):
raise GossipReject("invalid BLS to execution change signature")

# Mark this BTEC as seen
seen.bls_to_execution_change_indices.add(validator_index)
```

#### Transitioning the gossip

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
with_phases,
with_presets,
)
from eth_consensus_specs.test.helpers.constants import ALTAIR, MAINNET
from eth_consensus_specs.test.helpers.constants import ALTAIR, CAPELLA, 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
Expand Down Expand Up @@ -137,7 +137,7 @@ def run_validate_contribution_gossip(
return "reject", str(e)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
Comment thread
jihoonsong marked this conversation as resolved.
Outdated
@spec_state_test
@always_bls
def test_gossip_sync_committee_contribution_and_proof__valid(spec, state):
Expand Down Expand Up @@ -181,7 +181,7 @@ def test_gossip_sync_committee_contribution_and_proof__valid(spec, state):
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@spec_state_test
@always_bls
def test_gossip_sync_committee_contribution_and_proof__valid_at_period_boundary(spec, state):
Expand Down Expand Up @@ -231,7 +231,7 @@ def test_gossip_sync_committee_contribution_and_proof__valid_at_period_boundary(
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@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."""
Expand Down Expand Up @@ -283,7 +283,7 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_future_slot(spec,
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@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."""
Expand Down Expand Up @@ -339,7 +339,7 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_past_slot(spec, st
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@spec_state_test
def test_gossip_sync_committee_contribution_and_proof__reject_invalid_subcommittee_index(
spec, state
Expand Down Expand Up @@ -394,7 +394,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_invalid_subcommitt
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@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."""
Expand Down Expand Up @@ -448,7 +448,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_no_participants(sp
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@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):
Expand Down Expand Up @@ -519,7 +519,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_not_aggregator(spe
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@spec_state_test
def test_gossip_sync_committee_contribution_and_proof__reject_aggregator_not_in_subcommittee(
spec, state
Expand Down Expand Up @@ -577,7 +577,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_aggregator_not_in_
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@spec_state_test
def test_gossip_sync_committee_contribution_and_proof__reject_aggregator_index_out_of_range(
spec, state
Expand Down Expand Up @@ -631,7 +631,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_aggregator_index_o
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@spec_state_test
@always_bls
def test_gossip_sync_committee_contribution_and_proof__ignore_superset_contribution(spec, state):
Expand Down Expand Up @@ -755,7 +755,7 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_superset_contribut
yield "messages", "meta", messages


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@spec_state_test
@always_bls
def test_gossip_sync_committee_contribution_and_proof__valid_non_superset_contribution(spec, state):
Expand Down Expand Up @@ -874,7 +874,7 @@ def test_gossip_sync_committee_contribution_and_proof__valid_non_superset_contri
yield "messages", "meta", messages


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@spec_state_test
@always_bls
def test_gossip_sync_committee_contribution_and_proof__ignore_duplicate_aggregator(spec, state):
Expand Down Expand Up @@ -951,7 +951,7 @@ def test_gossip_sync_committee_contribution_and_proof__ignore_duplicate_aggregat
yield "messages", "meta", messages


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@spec_state_test
@always_bls
def test_gossip_sync_committee_contribution_and_proof__reject_invalid_selection_proof(spec, state):
Expand Down Expand Up @@ -1012,7 +1012,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_invalid_selection_
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@spec_state_test
@always_bls
def test_gossip_sync_committee_contribution_and_proof__reject_invalid_aggregator_signature(
Expand Down Expand Up @@ -1072,7 +1072,7 @@ def test_gossip_sync_committee_contribution_and_proof__reject_invalid_aggregator
)


@with_phases([ALTAIR])
@with_phases([ALTAIR, CAPELLA])
@spec_state_test
@always_bls
def test_gossip_sync_committee_contribution_and_proof__reject_invalid_aggregate_signature(
Expand Down
Loading