From 4920c9a551cafa3e4e7fb69fcd99ca36172ed2a3 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Oct 2025 01:51:03 +0200 Subject: [PATCH 1/2] eip7688: use forward compatible SSZ types in Gloas EIP-4788 exposed the beacon root to smart contracts, but smart contracts need to be redeployed / upgraded whenever generalized indices change during a fork, even if that fork does not touch any used functionality. That is analogous to an OS without ABI stability, requiring programs to be maintained and re-compiled due to random breakages in OS updates. This issue expands further to bridges on other blockchains, and also into wallets / dApps that verify data from the beacon chain instead. Such projects do not typically share Ethereum's release cadence. - https://eips.ethereum.org/EIPS/eip-4788 EIP-7688 introduces forward compatibility for beacon chain structures. Generalized indices remain same when list capacities evolve over forks, containers no longer get re-indexed when reaching a new power-of-2 number of fields, and fields can be deprecated, leaving a gap in the Merkle tree instead of triggering re-indexing. - https://eips.ethereum.org/EIPS/eip-7688 EIP-7688 was requested for inclusion by popular projects: - For Electra by Rocketpool: https://xcancel.com/KaneWallmann/status/1816729724145795258 - For Fulu by Lido: https://github.com/ethereum/pm/issues/1356#issuecomment-2733443137 --- presets/mainnet/gloas.yaml | 5 - presets/minimal/gloas.yaml | 5 - pyproject.toml | 2 +- pysetup/generate_specs.py | 4 +- pysetup/helpers.py | 83 ++++++--- pysetup/md_to_spec.py | 41 +++-- pysetup/spec_builders/gloas.py | 2 + pysetup/typing.py | 1 - specs/electra/beacon-chain.md | 23 ++- specs/electra/validator.md | 26 ++- specs/gloas/beacon-chain.md | 169 +++++++++++++++--- specs/gloas/builder.md | 4 +- specs/gloas/fork.md | 58 +++++- specs/gloas/light-client/sync-protocol.md | 32 ++++ specs/gloas/p2p-interface.md | 28 ++- .../test_process_execution_payload.py | 4 +- .../test/deneb/transition/test_operations.py | 2 + .../merkle_proof/test_single_merkle_proof.py | 4 +- .../test/fulu/unittests/test_networking.py | 6 +- .../test_process_execution_payload.py | 113 +++++++++--- .../test_process_execution_payload_bid.py | 4 +- .../eth2spec/test/gloas/sanity/test_blocks.py | 164 +++++++++++++++++ .../eth2spec/test/helpers/attestations.py | 25 ++- .../test/helpers/attester_slashings.py | 37 +++- .../core/pyspec/eth2spec/test/helpers/blob.py | 4 +- .../test/helpers/execution_payload.py | 6 +- .../eth2spec/test/helpers/fork_transition.py | 5 + .../pyspec/eth2spec/test/helpers/genesis.py | 2 +- .../core/pyspec/eth2spec/test/helpers/keys.py | 2 +- .../eth2spec/test/helpers/multi_operations.py | 5 +- .../test/helpers/proposer_slashings.py | 13 ++ .../pyspec/eth2spec/test/helpers/rewards.py | 7 +- .../test_process_attester_slashing.py | 26 +++ .../test/phase0/sanity/test_blocks.py | 11 +- .../test/phase0/ssz_static/test_ssz_static.py | 8 +- tests/infra/test_md_to_spec.py | 24 +-- uv.lock | 4 +- 37 files changed, 758 insertions(+), 201 deletions(-) create mode 100644 specs/gloas/light-client/sync-protocol.md create mode 100644 tests/core/pyspec/eth2spec/test/gloas/sanity/test_blocks.py diff --git a/presets/mainnet/gloas.yaml b/presets/mainnet/gloas.yaml index a636489746..a289e136da 100644 --- a/presets/mainnet/gloas.yaml +++ b/presets/mainnet/gloas.yaml @@ -9,8 +9,3 @@ PTC_SIZE: 512 # --------------------------------------------------------------- # 2**2 (= 4) attestations MAX_PAYLOAD_ATTESTATIONS: 4 - -# State list lengths -# --------------------------------------------------------------- -# 2**20 (= 1,048,576) builder pending withdrawals -BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576 diff --git a/presets/minimal/gloas.yaml b/presets/minimal/gloas.yaml index a6ace1b505..fad79d83da 100644 --- a/presets/minimal/gloas.yaml +++ b/presets/minimal/gloas.yaml @@ -9,8 +9,3 @@ PTC_SIZE: 2 # --------------------------------------------------------------- # 2**2 (= 4) attestations MAX_PAYLOAD_ATTESTATIONS: 4 - -# State list lengths -# --------------------------------------------------------------- -# 2**20 (= 1,048,576) builder pending withdrawals -BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576 diff --git a/pyproject.toml b/pyproject.toml index f17081d4fa..501a40e83a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "py_arkworks_bls12381==0.3.8", "py_ecc==8.0.0", "pycryptodome==3.23.0", - "remerkleable @ git+https://github.com/ethereum/remerkleable@92dcbb6f0507035d6875986fc263a05fec19d473", + "remerkleable @ git+https://github.com/ethereum/remerkleable@71a94389375aa9afbe39145dcd26d4cddafa140d", "ruamel.yaml==0.18.15", "setuptools==80.9.0", "trie==3.1.0", diff --git a/pysetup/generate_specs.py b/pysetup/generate_specs.py index 0556b9f7f6..bcfe1d5198 100644 --- a/pysetup/generate_specs.py +++ b/pysetup/generate_specs.py @@ -43,6 +43,7 @@ from pysetup.helpers import ( combine_spec_objects, dependency_order_class_objects, + finalized_spec_object, objects_to_spec, parse_config_vars, ) @@ -118,6 +119,7 @@ def build_spec( spec_object = all_specs[0] for value in all_specs[1:]: spec_object = combine_spec_objects(spec_object, value) + spec_object = finalized_spec_object(spec_object) class_objects = {**spec_object.ssz_objects, **spec_object.dataclasses} @@ -127,7 +129,7 @@ def build_spec( new_objects = copy.deepcopy(class_objects) dependency_order_class_objects( class_objects, - spec_object.custom_types | spec_object.preset_dep_custom_types, + spec_object.custom_types, ) return objects_to_spec(preset_name, spec_object, fork, class_objects) diff --git a/pysetup/helpers.py b/pysetup/helpers.py index 6c0d8cb40d..45b9b21be5 100644 --- a/pysetup/helpers.py +++ b/pysetup/helpers.py @@ -23,8 +23,19 @@ def collect_prev_forks(fork: str) -> list[str]: def requires_mypy_type_ignore(value: str) -> bool: - return value.startswith("ByteVector") or ( - value.startswith("Vector") and any(k in value for k in ["ceillog2", "floorlog2"]) + return ( + value.startswith("Bitlist") + or value.startswith("ByteVector") + or (value.startswith("List") and not re.match(r"^List\[\w+,\s*\w+\]$", value)) + or (value.startswith("Vector") and any(k in value for k in ["ceillog2", "floorlog2"])) + ) + + +def gen_new_type_definition(name: str, value: str) -> str: + return ( + f"class {name}({value}):\n pass" + if not requires_mypy_type_ignore(value) + else f"class {name}(\n {value} # type: ignore\n):\n pass" ) @@ -41,19 +52,11 @@ def objects_to_spec( """ def gen_new_type_definitions(custom_types: dict[str, str]) -> str: - return "\n\n".join( - [ - ( - f"class {key}({value}):\n pass\n" - if not requires_mypy_type_ignore(value) - else f"class {key}({value}): # type: ignore\n pass\n" - ) - for key, value in custom_types.items() - ] + return "\n\n\n".join( + [gen_new_type_definition(key, value) for key, value in custom_types.items()] ) new_type_definitions = gen_new_type_definitions(spec_object.custom_types) - preset_dep_new_type_definitions = gen_new_type_definitions(spec_object.preset_dep_custom_types) # Collect builders with the reversed previous forks # e.g. `[bellatrix, altair, phase0]` -> `[phase0, altair, bellatrix]` @@ -224,10 +227,9 @@ def format_constant(name: str, vardef: VariableDefinition) -> str: ssz_dep_constants, new_type_definitions, constant_vars_spec, - # The presets that some SSZ types require. Need to be defined before `preset_dep_new_type_definitions` + # The presets that some SSZ types require. preset_vars_spec, preset_dep_constant_vars_spec, - preset_dep_new_type_definitions, config_spec, # Custom classes which are not required to be SSZ containers. classes, @@ -267,8 +269,6 @@ def combine_dicts(old_dict: dict[str, T], new_dict: dict[str, T]) -> dict[str, T "bit", "Bitlist", "Bitvector", - "BLSPubkey", - "BLSSignature", "boolean", "byte", "ByteList", @@ -292,6 +292,8 @@ def combine_dicts(old_dict: dict[str, T], new_dict: dict[str, T]) -> dict[str, T "floorlog2", "List", "Optional", + "ProgressiveBitlist", + "ProgressiveList", "Sequence", "Set", "Tuple", @@ -312,10 +314,14 @@ def dependency_order_class_objects(objects: dict[str, str], custom_types: dict[s items = list(objects.items()) for key, value in items: dependencies = [] - for line in value.split("\n"): - if not re.match(r"\s+\w+: .+", line): + for i, line in enumerate(value.split("\n")): + if i == 0: + match = re.match(r".+\((.+)\):", line) + else: + match = re.match(r"\s+\w+: (.+)", line) + if not match: continue # skip whitespace etc. - line = line[line.index(":") + 1 :] # strip of field name + line = match.group(1) if "#" in line: line = line[: line.index("#")] # strip of comment dependencies.extend( @@ -349,9 +355,6 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject: protocols = combine_protocols(spec0.protocols, spec1.protocols) functions = combine_dicts(spec0.functions, spec1.functions) custom_types = combine_dicts(spec0.custom_types, spec1.custom_types) - preset_dep_custom_types = combine_dicts( - spec0.preset_dep_custom_types, spec1.preset_dep_custom_types - ) constant_vars = combine_dicts(spec0.constant_vars, spec1.constant_vars) preset_dep_constant_vars = combine_dicts( spec0.preset_dep_constant_vars, spec1.preset_dep_constant_vars @@ -366,7 +369,6 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject: functions=functions, protocols=protocols, custom_types=custom_types, - preset_dep_custom_types=preset_dep_custom_types, constant_vars=constant_vars, preset_dep_constant_vars=preset_dep_constant_vars, preset_vars=preset_vars, @@ -378,6 +380,41 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject: ) +def finalized_spec_object(spec_object: SpecObject) -> SpecObject: + all_config_dependencies = { + vardef.type_name or vardef.type_hint + for vardef in ( + spec_object.constant_vars + | spec_object.preset_dep_constant_vars + | spec_object.preset_vars + | spec_object.config_vars + ).values() + if (vardef.type_name or vardef.type_hint) is not None + } + + custom_types = {} + ssz_objects = spec_object.ssz_objects + for name, value in spec_object.custom_types.items(): + if any(k in name for k in all_config_dependencies): + custom_types[name] = value + else: + ssz_objects[name] = gen_new_type_definition(name, value) + + return SpecObject( + functions=spec_object.functions, + protocols=spec_object.protocols, + custom_types=custom_types, + constant_vars=spec_object.constant_vars, + preset_dep_constant_vars=spec_object.preset_dep_constant_vars, + preset_vars=spec_object.preset_vars, + config_vars=spec_object.config_vars, + ssz_dep_constants=spec_object.ssz_dep_constants, + func_dep_presets=spec_object.func_dep_presets, + ssz_objects=ssz_objects, + dataclasses=spec_object.dataclasses, + ) + + def parse_config_vars(conf: dict[str, str]) -> dict[str, str | list[dict[str, str]]]: """ Parses a dict of basic str/int/list types into a dict for insertion into the spec code. diff --git a/pysetup/md_to_spec.py b/pysetup/md_to_spec.py index b5f2a82008..6b55243d4b 100644 --- a/pysetup/md_to_spec.py +++ b/pysetup/md_to_spec.py @@ -32,7 +32,6 @@ def __init__( self.preset_name = preset_name self.document_iterator: Iterator[Element] = self._parse_document(file_name) - self.all_custom_types: dict[str, str] = {} self.current_heading_name: str | None = None # Use a single dict to hold all SpecObject fields @@ -44,7 +43,6 @@ def __init__( "func_dep_presets": {}, "functions": {}, "preset_dep_constant_vars": {}, - "preset_dep_custom_types": {}, "preset_vars": {}, "protocols": {}, "ssz_dep_constants": {}, @@ -180,8 +178,12 @@ def _process_code_class(self, source: str, cls: ast.ClassDef) -> None: if class_name != self.current_heading_name: raise Exception(f"class_name {class_name} != current_name {self.current_heading_name}") - if parent_class: - assert parent_class == "Container" + if parent_class == "ProgressiveContainer": + source = re.sub( + r"^(.*ProgressiveContainer.*)$", r"\1 # type: ignore", source, flags=re.MULTILINE + ) + else: + assert parent_class is None or parent_class == "Container" self.spec["ssz_objects"][class_name] = source def _process_table(self, table: Table) -> None: @@ -202,9 +204,21 @@ def _process_table(self, table: Table) -> None: if not _is_constant_id(name): # Check for short type declarations if value.startswith( - ("uint", "Bytes", "ByteList", "Union", "Vector", "List", "ByteVector") + ( + "uint", + "Bitlist", + "Bitvector", + "ByteList", + "ByteVector", + "Bytes", + "List", + "ProgressiveBitlist", + "ProgressiveList", + "Union", + "Vector", + ) ): - self.all_custom_types[name] = value + self.spec["custom_types"][name] = value continue # It is a constant name and a generalized index @@ -418,7 +432,6 @@ def _process_html_block(self, html: HTMLBlock) -> None: def _finalize_types(self) -> None: """ - Processes all_custom_types into custom_types and preset_dep_custom_types. Calls helper functions to update KZG and CURDLEPROOFS setups if needed. """ # Update KZG trusted setup if needed @@ -433,17 +446,6 @@ def _finalize_types(self) -> None: self.spec["constant_vars"], self.preset_name ) - # Split all_custom_types into custom_types and preset_dep_custom_types - self.spec["custom_types"] = {} - self.spec["preset_dep_custom_types"] = {} - for name, value in self.all_custom_types.items(): - if any(k in value for k in self.preset) or any( - k in value for k in self.spec["preset_dep_constant_vars"] - ): - self.spec["preset_dep_custom_types"][name] = value - else: - self.spec["custom_types"][name] = value - def _build_spec_object(self) -> SpecObject: """ Returns the SpecObject using all collected data. @@ -456,7 +458,6 @@ def _build_spec_object(self) -> SpecObject: func_dep_presets=self.spec["func_dep_presets"], functions=self.spec["functions"], preset_dep_constant_vars=self.spec["preset_dep_constant_vars"], - preset_dep_custom_types=self.spec["preset_dep_custom_types"], preset_vars=self.spec["preset_vars"], protocols=self.spec["protocols"], ssz_dep_constants=self.spec["ssz_dep_constants"], @@ -496,6 +497,8 @@ def _get_class_info_from_ast(cls: ast.ClassDef) -> tuple[str, str | None]: parent_class = base.id elif isinstance(base, ast.Subscript): parent_class = base.value.id + elif isinstance(base, ast.Call): + parent_class = base.func.id else: # NOTE: SSZ definition derives from earlier phase... # e.g. `phase0.SignedBeaconBlock` diff --git a/pysetup/spec_builders/gloas.py b/pysetup/spec_builders/gloas.py index 48cef3b47b..8bc1e8f0bb 100644 --- a/pysetup/spec_builders/gloas.py +++ b/pysetup/spec_builders/gloas.py @@ -8,6 +8,8 @@ class GloasSpecBuilder(BaseSpecBuilder): @classmethod def imports(cls, preset_name: str): return f""" +from eth2spec.utils.ssz.ssz_typing import ProgressiveBitlist, ProgressiveContainer, ProgressiveList + from eth2spec.fulu import {preset_name} as fulu """ diff --git a/pysetup/typing.py b/pysetup/typing.py index 4c7df5712c..7e83680954 100644 --- a/pysetup/typing.py +++ b/pysetup/typing.py @@ -18,7 +18,6 @@ class SpecObject(NamedTuple): functions: dict[str, str] protocols: dict[str, ProtocolDefinition] custom_types: dict[str, str] - preset_dep_custom_types: dict[str, str] # the types that depend on presets constant_vars: dict[str, VariableDefinition] preset_dep_constant_vars: dict[str, VariableDefinition] preset_vars: dict[str, VariableDefinition] diff --git a/specs/electra/beacon-chain.md b/specs/electra/beacon-chain.md index 66dfa5fe51..96b5227985 100644 --- a/specs/electra/beacon-chain.md +++ b/specs/electra/beacon-chain.md @@ -3,6 +3,7 @@ - [Introduction](#introduction) +- [Custom types](#custom-types) - [Constants](#constants) - [Misc](#misc) - [Withdrawal prefixes](#withdrawal-prefixes) @@ -123,6 +124,16 @@ Electra is a consensus-layer upgrade containing a number of features. Including: *Note*: This specification is built upon [Deneb](../deneb/beacon-chain.md) and is under active development. +## Custom types + +| Name | SSZ equivalent | Description | +| ----------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------ | +| `AggregationBits` | `Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]` | Combined participation info across all participating subcommittees | +| `AttestingIndices` | `List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]` | List of attesting validator indices | +| `DepositRequests` | `List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]` | List of deposit requests pertaining to an execution payload | +| `WithdrawalRequests` | `List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD]` | List of withdrawal requests pertaining to an execution payload | +| `ConsolidationRequests` | `List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD]` | List of withdrawal requests pertaining to an execution payload | + ## Constants The following values are (non-configurable) constants used throughout the @@ -293,11 +304,11 @@ class ConsolidationRequest(Container): ```python class ExecutionRequests(Container): # [New in Electra:EIP6110] - deposits: List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD] + deposits: DepositRequests # [New in Electra:EIP7002:EIP7251] - withdrawals: List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD] + withdrawals: WithdrawalRequests # [New in Electra:EIP7251] - consolidations: List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD] + consolidations: ConsolidationRequests ``` #### `SingleAttestation` @@ -351,7 +362,7 @@ class BeaconBlockBody(Container): ```python class Attestation(Container): # [Modified in Electra:EIP7549] - aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] + aggregation_bits: AggregationBits data: AttestationData signature: BLSSignature # [New in Electra:EIP7549] @@ -363,7 +374,7 @@ class Attestation(Container): ```python class IndexedAttestation(Container): # [Modified in Electra:EIP7549] - attesting_indices: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] + attesting_indices: AttestingIndices data: AttestationData signature: BLSSignature ``` @@ -1320,7 +1331,7 @@ def get_execution_requests_list(execution_requests: ExecutionRequests) -> Sequen return [ request_type + ssz_serialize(request_data) for request_type, request_data in requests - if len(request_data) != 0 + if request_data ] ``` diff --git a/specs/electra/validator.md b/specs/electra/validator.md index deebb03f15..4ceb29bb4e 100644 --- a/specs/electra/validator.md +++ b/specs/electra/validator.md @@ -128,7 +128,7 @@ def compute_on_chain_aggregate(network_aggregates: Sequence[Attestation]) -> Att ) data = aggregates[0].data - aggregation_bits = Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]() + aggregation_bits = AggregationBits() for a in aggregates: for b in a.aggregation_bits: aggregation_bits.append(b) @@ -292,17 +292,11 @@ def get_execution_requests(execution_requests_list: Sequence[bytes]) -> Executio prev_request_type = request_type if request_type == DEPOSIT_REQUEST_TYPE: - deposits = ssz_deserialize( - List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD], request_data - ) + deposits = ssz_deserialize(DepositRequests, request_data) elif request_type == WITHDRAWAL_REQUEST_TYPE: - withdrawals = ssz_deserialize( - List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD], request_data - ) + withdrawals = ssz_deserialize(WithdrawalRequests, request_data) elif request_type == CONSOLIDATION_REQUEST_TYPE: - consolidations = ssz_deserialize( - List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD], request_data - ) + consolidations = ssz_deserialize(ConsolidationRequests, request_data) return ExecutionRequests( deposits=deposits, @@ -339,9 +333,9 @@ updated field assignments: ### Construct aggregate - Set `attestation_data.index = 0`. -- Let `aggregation_bits` be a - `Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]` of length - `len(committee)`, where each bit set from each individual attestation is set - to `0b1`. -- Set `attestation.committee_bits = committee_bits`, where `committee_bits` has - the bit set corresponding to `committee_index` in each individual attestation. +- Set `aggregate_attestation.aggregation_bits` to an `AggregationBits` of length + `len(committee)` with the bits corresponding to the `attester_index` of each + `SingleAttestation` inputs set to `0b1`. +- Set `aggregate_attestation.committee_bits` to a + `Bitvector[MAX_COMMITTEES_PER_SLOT]` with the single bit corresponding to the + shared `committee_index` across all `SingleAttestation` inputs set to `0b1`. diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 3eeb35d9c7..bb61c2fce5 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -5,6 +5,7 @@ - [Introduction](#introduction) +- [Custom types](#custom-types) - [Constants](#constants) - [Domain types](#domain-types) - [Misc](#misc) @@ -12,7 +13,6 @@ - [Preset](#preset) - [Misc](#misc-1) - [Max operations per block](#max-operations-per-block) - - [State list lengths](#state-list-lengths) - [Containers](#containers) - [New containers](#new-containers) - [`BuilderPendingPayment`](#builderpendingpayment) @@ -26,10 +26,15 @@ - [`ExecutionPayloadEnvelope`](#executionpayloadenvelope) - [`SignedExecutionPayloadEnvelope`](#signedexecutionpayloadenvelope) - [Modified containers](#modified-containers) + - [`Attestation`](#attestation) + - [`IndexedAttestation`](#indexedattestation) - [`BeaconBlockBody`](#beaconblockbody) + - [`ExecutionPayload`](#executionpayload) + - [`ExecutionRequests`](#executionrequests) - [`BeaconState`](#beaconstate) - [Helper functions](#helper-functions) - [Predicates](#predicates) + - [Modified `is_valid_indexed_attestation`](#modified-is_valid_indexed_attestation) - [New `has_builder_withdrawal_credential`](#new-has_builder_withdrawal_credential) - [Modified `has_compounding_withdrawal_credential`](#modified-has_compounding_withdrawal_credential) - [New `is_attestation_same_slot`](#new-is_attestation_same_slot) @@ -107,6 +112,18 @@ At any given slot, the status of the blockchain's head may be either - A full block for the current slot (both the proposer and the builder revealed on time). +## Custom types + +*[Modified in Gloas:EIP7688]* + +| Name | SSZ equivalent | Description | +| ----------------------- | --------------------------------------- | --------------------------------------------------------------- | +| `AggregationBits` | `ProgressiveBitlist` | Combined participation info for all participating subcommittees | +| `AttestingIndices` | `ProgressiveList[ValidatorIndex]` | List of attesting validator indices | +| `DepositRequests` | `ProgressiveList[DepositRequest]` | List of deposit requests pertaining to an execution payload | +| `WithdrawalRequests` | `ProgressiveList[WithdrawalRequest]` | List of withdrawal requests pertaining to an execution payload | +| `ConsolidationRequests` | `ProgressiveList[ConsolidationRequest]` | List of withdrawal requests pertaining to an execution payload | + ## Constants ### Domain types @@ -143,12 +160,6 @@ At any given slot, the status of the blockchain's head may be either | -------------------------- | ----- | | `MAX_PAYLOAD_ATTESTATIONS` | `4` | -### State list lengths - -| Name | Value | Unit | -| ----------------------------------- | ----------------------------- | --------------------------- | -| `BUILDER_PENDING_WITHDRAWALS_LIMIT` | `uint64(2**20)` (= 1,048,576) | Builder pending withdrawals | - ## Containers ### New containers @@ -240,7 +251,7 @@ class ExecutionPayloadEnvelope(Container): builder_index: ValidatorIndex beacon_block_root: Root slot: Slot - blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + blob_kzg_commitments: ProgressiveList[KZGCommitment] state_root: Root ``` @@ -254,6 +265,27 @@ class SignedExecutionPayloadEnvelope(Container): ### Modified containers +#### `Attestation` + +```python +# [Modified in Gloas:EIP7688] +class Attestation(ProgressiveContainer(active_fields=[1] * 4)): + aggregation_bits: AggregationBits + data: AttestationData + signature: BLSSignature + committee_bits: Bitvector[MAX_COMMITTEES_PER_SLOT] +``` + +#### `IndexedAttestation` + +```python +# [Modified in Gloas:EIP7688] +class IndexedAttestation(ProgressiveContainer(active_fields=[1] * 3)): + attesting_indices: AttestingIndices + data: AttestationData + signature: BLSSignature +``` + #### `BeaconBlockBody` *Note*: The `BeaconBlockBody` container is modified to contain a @@ -263,19 +295,20 @@ removed from the beacon block body and moved into the signed execution payload envelope. ```python -class BeaconBlockBody(Container): +# [Modified in Gloas:EIP7688] +class BeaconBlockBody(ProgressiveContainer(active_fields=[1] * 12)): randao_reveal: BLSSignature eth1_data: Eth1Data graffiti: Bytes32 - proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] - attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS_ELECTRA] - attestations: List[Attestation, MAX_ATTESTATIONS_ELECTRA] - deposits: List[Deposit, MAX_DEPOSITS] - voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + proposer_slashings: ProgressiveList[ProposerSlashing] + attester_slashings: ProgressiveList[AttesterSlashing] + attestations: ProgressiveList[Attestation] + deposits: ProgressiveList[Deposit] + voluntary_exits: ProgressiveList[SignedVoluntaryExit] sync_aggregate: SyncAggregate # [Modified in Gloas:EIP7732] # Removed `execution_payload` - bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] + bls_to_execution_changes: ProgressiveList[SignedBLSToExecutionChange] # [Modified in Gloas:EIP7732] # Removed `blob_kzg_commitments` # [Modified in Gloas:EIP7732] @@ -283,7 +316,43 @@ class BeaconBlockBody(Container): # [New in Gloas:EIP7732] signed_execution_payload_bid: SignedExecutionPayloadBid # [New in Gloas:EIP7732] - payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] + payload_attestations: ProgressiveList[PayloadAttestation] +``` + +#### `ExecutionPayload` + +```python +# [Modified in Gloas:EIP7688] +class ExecutionPayload(ProgressiveContainer(active_fields=[1] * 17)): + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + block_hash: Hash32 + # [Modified in Gloas:EIP7688] + transactions: ProgressiveList[Transaction] + # [Modified in Gloas:EIP7688] + withdrawals: ProgressiveList[Withdrawal] + blob_gas_used: uint64 + excess_blob_gas: uint64 +``` + +#### `ExecutionRequests` + +```python +# [Modified in Gloas:EIP7688] +class ExecutionRequests(ProgressiveContainer(active_fields=[1] * 3)): + deposits: DepositRequests + withdrawals: WithdrawalRequests + consolidations: ConsolidationRequests ``` #### `BeaconState` @@ -296,7 +365,8 @@ committed block hash and the last slot that was full, that is in which there were both consensus and execution blocks included. ```python -class BeaconState(Container): +# [Modified in Gloas:EIP7688] +class BeaconState(ProgressiveContainer(active_fields=[1] * 43)): genesis_time: uint64 genesis_validators_root: Root slot: Slot @@ -308,17 +378,22 @@ class BeaconState(Container): eth1_data: Eth1Data eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] eth1_deposit_index: uint64 - validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] - balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + # [Modified in Gloas:EIP7688] + validators: ProgressiveList[Validator] + # [Modified in Gloas:EIP7688] + balances: ProgressiveList[Gwei] randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] - previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] - current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + # [Modified in Gloas:EIP7688] + previous_epoch_participation: ProgressiveList[ParticipationFlags] + # [Modified in Gloas:EIP7688] + current_epoch_participation: ProgressiveList[ParticipationFlags] justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] previous_justified_checkpoint: Checkpoint current_justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint - inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + # [Modified in Gloas:EIP7688] + inactivity_scores: ProgressiveList[uint64] current_sync_committee: SyncCommittee next_sync_committee: SyncCommittee # [Modified in Gloas:EIP7732] @@ -334,16 +409,19 @@ class BeaconState(Container): earliest_exit_epoch: Epoch consolidation_balance_to_consume: Gwei earliest_consolidation_epoch: Epoch - pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT] - pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT] - pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT] + # [Modified in Gloas:EIP7688] + pending_deposits: ProgressiveList[PendingDeposit] + # [Modified in Gloas:EIP7688] + pending_partial_withdrawals: ProgressiveList[PendingPartialWithdrawal] + # [Modified in Gloas:EIP7688] + pending_consolidations: ProgressiveList[PendingConsolidation] proposer_lookahead: Vector[ValidatorIndex, (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH] # [New in Gloas:EIP7732] execution_payload_availability: Bitvector[SLOTS_PER_HISTORICAL_ROOT] # [New in Gloas:EIP7732] builder_pending_payments: Vector[BuilderPendingPayment, 2 * SLOTS_PER_EPOCH] # [New in Gloas:EIP7732] - builder_pending_withdrawals: List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT] + builder_pending_withdrawals: ProgressiveList[BuilderPendingWithdrawal] # [New in Gloas:EIP7732] latest_block_hash: Hash32 # [New in Gloas:EIP7732] @@ -354,6 +432,31 @@ class BeaconState(Container): ### Predicates +#### Modified `is_valid_indexed_attestation` + +```python +def is_valid_indexed_attestation( + state: BeaconState, indexed_attestation: IndexedAttestation +) -> bool: + """ + Check if ``indexed_attestation`` is not empty, has sorted and unique indices and has a valid aggregate signature. + """ + # [Modified in Gloas:EIP7688] + # Verify indices are sorted and unique + indices = indexed_attestation.attesting_indices + if ( + len(indices) == 0 + or len(indices) > MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT + or not indices == sorted(set(indices)) + ): + return False + # Verify aggregate signature + pubkeys = [state.validators[i].pubkey for i in indices] + domain = get_domain(state, DOMAIN_BEACON_ATTESTER, indexed_attestation.data.target.epoch) + signing_root = compute_signing_root(indexed_attestation.data, domain) + return bls.FastAggregateVerify(pubkeys, signing_root, indexed_attestation.signature) +``` + #### New `has_builder_withdrawal_credential` ```python @@ -925,7 +1028,7 @@ def process_withdrawals(state: BeaconState) -> None: withdrawals, processed_builder_withdrawals_count, processed_partial_withdrawals_count = ( get_expected_withdrawals(state) ) - withdrawals_list = List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD](withdrawals) + withdrawals_list = ProgressiveList[Withdrawal](withdrawals) state.latest_withdrawals_root = hash_tree_root(withdrawals_list) for withdrawal in withdrawals: decrease_balance(state, withdrawal.validator_index, withdrawal.amount) @@ -1056,6 +1159,13 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: for operation in operations: fn(state, operation) + # [Modified in Gloas:EIP7688] + assert len(body.proposer_slashings) <= MAX_PROPOSER_SLASHINGS + assert len(body.attester_slashings) <= MAX_ATTESTER_SLASHINGS_ELECTRA + assert len(body.attestations) <= MAX_ATTESTATIONS_ELECTRA + assert len(body.voluntary_exits) <= MAX_VOLUNTARY_EXITS + assert len(body.bls_to_execution_changes) <= MAX_BLS_TO_EXECUTION_CHANGES + assert len(body.payload_attestations) <= MAX_PAYLOAD_ATTESTATIONS # [Modified in Gloas:EIP7732] for_ops(body.proposer_slashings, process_proposer_slashing) for_ops(body.attester_slashings, process_attester_slashing) @@ -1230,7 +1340,7 @@ blob kzg commitments root for an empty list ```python def is_merge_transition_complete(state: BeaconState) -> bool: bid = ExecutionPayloadBid() - kzgs = List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]() + kzgs = ProgressiveList[KZGCommitment]() bid.blob_kzg_commitments_root = kzgs.hash_tree_root() return state.latest_execution_payload_bid != bid @@ -1352,6 +1462,9 @@ def process_execution_payload( for operation in operations: fn(state, operation) + assert len(requests.deposits) <= MAX_DEPOSIT_REQUESTS_PER_PAYLOAD + assert len(requests.withdrawals) <= MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD + assert len(requests.consolidations) <= MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD for_ops(requests.deposits, process_deposit_request) for_ops(requests.withdrawals, process_withdrawal_request) for_ops(requests.consolidations, process_consolidation_request) diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 2ad0113fda..7f0b30b4cb 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -148,7 +148,7 @@ and inclusion proof verifications are no longer required in ePBS. ```python def get_data_column_sidecars( beacon_block_root: Root, - kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK], + kzg_commitments: ProgressiveList[KZGCommitment], cells_and_kzg_proofs: Sequence[ Tuple[Vector[Cell, CELLS_PER_EXT_BLOB], Vector[KZGProof, CELLS_PER_EXT_BLOB]] ], @@ -187,7 +187,7 @@ of header and inclusion proof computations. def get_data_column_sidecars_from_block( signed_block: SignedBeaconBlock, # [New in Gloas:EIP7732] - blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK], + blob_kzg_commitments: ProgressiveList[KZGCommitment], cells_and_kzg_proofs: Sequence[ Tuple[Vector[Cell, CELLS_PER_EXT_BLOB], Vector[KZGProof, CELLS_PER_EXT_BLOB]] ], diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 99a5af003f..b87f82ef39 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -9,6 +9,7 @@ - [Fork to Gloas](#fork-to-gloas) - [Fork trigger](#fork-trigger) - [Upgrading the state](#upgrading-the-state) + - [Upgrading attestations and attester slashings](#upgrading-attestations-and-attester-slashings) @@ -58,17 +59,21 @@ def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState: eth1_data=pre.eth1_data, eth1_data_votes=pre.eth1_data_votes, eth1_deposit_index=pre.eth1_deposit_index, - validators=pre.validators, - balances=pre.balances, + validators=ProgressiveList[Validator](list(pre.validators)), + balances=ProgressiveList[Gwei](list(pre.balances)), randao_mixes=pre.randao_mixes, slashings=pre.slashings, - previous_epoch_participation=pre.previous_epoch_participation, - current_epoch_participation=pre.current_epoch_participation, + previous_epoch_participation=ProgressiveList[ParticipationFlags]( + list(pre.previous_epoch_participation) + ), + current_epoch_participation=ProgressiveList[ParticipationFlags]( + list(pre.current_epoch_participation) + ), justification_bits=pre.justification_bits, previous_justified_checkpoint=pre.previous_justified_checkpoint, current_justified_checkpoint=pre.current_justified_checkpoint, finalized_checkpoint=pre.finalized_checkpoint, - inactivity_scores=pre.inactivity_scores, + inactivity_scores=ProgressiveList[uint64](list(pre.inactivity_scores)), current_sync_committee=pre.current_sync_committee, next_sync_committee=pre.next_sync_committee, # [Modified in Gloas:EIP7732] @@ -84,9 +89,13 @@ def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState: earliest_exit_epoch=pre.earliest_exit_epoch, consolidation_balance_to_consume=pre.consolidation_balance_to_consume, earliest_consolidation_epoch=pre.earliest_consolidation_epoch, - pending_deposits=pre.pending_deposits, - pending_partial_withdrawals=pre.pending_partial_withdrawals, - pending_consolidations=pre.pending_consolidations, + pending_deposits=ProgressiveList[PendingDeposit](list(pre.pending_deposits)), + pending_partial_withdrawals=ProgressiveList[PendingPartialWithdrawal]( + list(pre.pending_partial_withdrawals) + ), + pending_consolidations=ProgressiveList[PendingConsolidation]( + list(pre.pending_consolidations) + ), proposer_lookahead=pre.proposer_lookahead, # [New in Gloas:EIP7732] execution_payload_availability=[0b1 for _ in range(SLOTS_PER_HISTORICAL_ROOT)], @@ -102,3 +111,36 @@ def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState: return post ``` + +### Upgrading attestations and attester slashings + +A Gloas `BeaconBlockBody` can still contain earlier attestations and attester +slashings. In order to pack them, the pre-Gloas data needs to be locally +upgraded to Gloas before creating the block. + +```python +def upgrade_attestation_to_gloas(pre: fulu.Attestation) -> Attestation: + return Attestation( + aggregation_bits=AggregationBits(list(pre.aggregation_bits)), + data=pre.data, + signature=pre.signature, + committee_bits=pre.committee_bits, + ) +``` + +```python +def upgrade_indexed_attestation_to_gloas(pre: fulu.IndexedAttestation) -> IndexedAttestation: + return IndexedAttestation( + attesting_indices=AttestingIndices(list(pre.attesting_indices)), + data=pre.data, + signature=pre.signature, + ) +``` + +```python +def upgrade_attester_slashing_to_gloas(pre: fulu.AttesterSlashing) -> AttesterSlashing: + return AttesterSlashing( + attestation_1=upgrade_indexed_attestation_to_gloas(pre.attestation_1), + attestation_2=upgrade_indexed_attestation_to_gloas(pre.attestation_2), + ) +``` diff --git a/specs/gloas/light-client/sync-protocol.md b/specs/gloas/light-client/sync-protocol.md new file mode 100644 index 0000000000..04e5960101 --- /dev/null +++ b/specs/gloas/light-client/sync-protocol.md @@ -0,0 +1,32 @@ +# Gloas Light Client -- Sync Protocol + + + +- [Introduction](#introduction) +- [Constants](#constants) + - [Frozen constants](#frozen-constants) +- [Note](#note) + + + +## Introduction + +This upgrade updates light client data to include the Gloas changes to the +generalized indices of [`BeaconState`](../beacon-chain.md). + +## Constants + +### Frozen constants + +Existing `GeneralizedIndex` constants are frozen at their +[Electra](../../electra/light-client/sync-protocol.md#constants) values. + +| Name | Value | +| --------------------------------------- | ------------------------------------------------------------------------------------ | +| `FINALIZED_ROOT_GINDEX_ELECTRA` | `get_generalized_index(electra.BeaconState, 'finalized_checkpoint', 'root')` (= 169) | +| `CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA` | `get_generalized_index(electra.BeaconState, 'current_sync_committee')` (= 86) | +| `NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA` | `get_generalized_index(electra.BeaconState, 'next_sync_committee')` (= 87) | + +## Note + +The light client specs for Gloas are currently incomplete. diff --git a/specs/gloas/p2p-interface.md b/specs/gloas/p2p-interface.md index de4fa04540..ef03244694 100644 --- a/specs/gloas/p2p-interface.md +++ b/specs/gloas/p2p-interface.md @@ -88,9 +88,12 @@ committed in the builder's bid for the corresponding `beacon_block_root`. ```python class DataColumnSidecar(Container): index: ColumnIndex - column: List[Cell, MAX_BLOB_COMMITMENTS_PER_BLOCK] - kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] - kzg_proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] + # [Modified in Gloas:7688] + column: ProgressiveList[Cell] + # [Modified in Gloas:7688] + kzg_commitments: ProgressiveList[KZGCommitment] + # [Modified in Gloas:7688] + kzg_proofs: ProgressiveList[KZGProof] beacon_block_root: Root ``` @@ -146,6 +149,14 @@ The *type* of the payload of this topic changes to the (modified) There are no new validations for this topic. However, all validations with regards to the `ExecutionPayload` are removed: +- _[REJECT]_ The number of operations is within limits -- i.e. validate that + `len(body.deposits) <= MAX_DEPOSITS` and + `len(body.proposer_slashings) <= MAX_PROPOSER_SLASHINGS` and + `len(body.attester_slashings) <= MAX_ATTESTER_SLASHINGS_ELECTRA` and + `len(body.attestations) <= MAX_ATTESTATIONS_ELECTRA` and + `len(body.voluntary_exits) <= MAX_VOLUNTARY_EXITS` and + `len(body.bls_to_execution_changes) <= MAX_BLS_TO_EXECUTION_CHANGES` and + `len(body.payload_attestations) <= MAX_PAYLOAD_ATTESTATIONS` - _[REJECT]_ The length of KZG commitments is less than or equal to the limitation defined in Consensus Layer -- i.e. validate that `len(signed_beacon_block.message.body.blob_kzg_commitments) <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block` @@ -182,8 +193,15 @@ This topic is used to propagate execution payload messages as 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 = payload_envelope.payload`: - +`payload = payload_envelope.payload`, +`execution_requests = payload_envelope.execution_requests`: + +- _[REJECT]_ The number of execution requests is within limits -- i.e. validate + that `len(execution_requests.deposits) <= MAX_DEPOSIT_REQUESTS_PER_PAYLOAD` + and + `len(execution_requests.withdrawals) <= MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD` + and + `len(execution_requests.consolidations) <= MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD` - _[IGNORE]_ The envelope's block root `envelope.block_root` has been seen (via gossip or non-gossip sources) (a client MAY queue payload for processing once the block is retrieved). diff --git a/tests/core/pyspec/eth2spec/test/deneb/block_processing/test_process_execution_payload.py b/tests/core/pyspec/eth2spec/test/deneb/block_processing/test_process_execution_payload.py index ea8b530f33..8369bdfff4 100644 --- a/tests/core/pyspec/eth2spec/test/deneb/block_processing/test_process_execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/deneb/block_processing/test_process_execution_payload.py @@ -38,9 +38,7 @@ def run_execution_payload_processing( slot=state.slot, builder_index=spec.get_beacon_proposer_index(state), ) - kzg_list = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( - blob_kzg_commitments - ) + kzg_list = spec.ProgressiveList[spec.KZGCommitment](blob_kzg_commitments) # In Gloas, blob_kzg_commitments_root is stored in latest_execution_payload_bid, not latest_execution_payload_header state.latest_execution_payload_bid.blob_kzg_commitments_root = kzg_list.hash_tree_root() state.latest_execution_payload_bid.builder_index = envelope.builder_index diff --git a/tests/core/pyspec/eth2spec/test/deneb/transition/test_operations.py b/tests/core/pyspec/eth2spec/test/deneb/transition/test_operations.py index 3904b1877d..31380443d5 100644 --- a/tests/core/pyspec/eth2spec/test/deneb/transition/test_operations.py +++ b/tests/core/pyspec/eth2spec/test/deneb/transition/test_operations.py @@ -5,6 +5,7 @@ ) from eth2spec.test.helpers.attestations import ( get_valid_attestation, + upgrade_attestation_to_new_spec, ) from eth2spec.test.helpers.block import ( build_empty_block_for_next_slot, @@ -112,6 +113,7 @@ def test_transition_attestation_from_previous_fork_with_new_range( # Transition to the fork epoch with a block transition_until_fork(spec, state, fork_epoch) state, fork_block = do_fork(state, spec, post_spec, fork_epoch) + attestation = upgrade_attestation_to_new_spec(spec, post_spec, attestation) current_epoch = spec.get_current_epoch(state) assert current_epoch == fork_epoch # Transition to second to last slot in `fork_epoch` diff --git a/tests/core/pyspec/eth2spec/test/fulu/merkle_proof/test_single_merkle_proof.py b/tests/core/pyspec/eth2spec/test/fulu/merkle_proof/test_single_merkle_proof.py index 84374f157c..ebb81dbaa8 100644 --- a/tests/core/pyspec/eth2spec/test/fulu/merkle_proof/test_single_merkle_proof.py +++ b/tests/core/pyspec/eth2spec/test/fulu/merkle_proof/test_single_merkle_proof.py @@ -38,9 +38,7 @@ def _run_blob_kzg_commitments_merkle_proof_test(spec, state, rng=None, blob_coun chaos=True, ) if is_post_gloas(spec): - blob_kzg_commitments = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( - blob_kzg_commitments - ) + blob_kzg_commitments = spec.ProgressiveList[spec.KZGCommitment](blob_kzg_commitments) kzg_root = blob_kzg_commitments.hash_tree_root() block.body.signed_execution_payload_bid.message.blob_kzg_commitments_root = kzg_root else: diff --git a/tests/core/pyspec/eth2spec/test/fulu/unittests/test_networking.py b/tests/core/pyspec/eth2spec/test/fulu/unittests/test_networking.py index 7acce29f10..5bcd6ee330 100644 --- a/tests/core/pyspec/eth2spec/test/fulu/unittests/test_networking.py +++ b/tests/core/pyspec/eth2spec/test/fulu/unittests/test_networking.py @@ -41,9 +41,9 @@ def compute_data_column_sidecar(spec, state): cells_and_kzg_proofs = [spec.compute_cells_and_kzg_proofs(blob) for blob in blobs] if is_post_gloas(spec): - block.body.signed_execution_payload_bid.message.blob_kzg_commitments_root = spec.List[ - spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK - ](blob_kzg_commitments).hash_tree_root() + block.body.signed_execution_payload_bid.message.blob_kzg_commitments_root = ( + spec.ProgressiveList[spec.KZGCommitment](blob_kzg_commitments).hash_tree_root() + ) signed_block = sign_block(spec, state, block, proposer_index=0) return spec.get_data_column_sidecars_from_block( signed_block, blob_kzg_commitments, cells_and_kzg_proofs diff --git a/tests/core/pyspec/eth2spec/test/gloas/block_processing/test_process_execution_payload.py b/tests/core/pyspec/eth2spec/test/gloas/block_processing/test_process_execution_payload.py index 39d260576d..43f3c11e10 100644 --- a/tests/core/pyspec/eth2spec/test/gloas/block_processing/test_process_execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/gloas/block_processing/test_process_execution_payload.py @@ -1,3 +1,5 @@ +from random import Random + from eth2spec.test.context import ( always_bls, spec_state_test, @@ -10,6 +12,11 @@ build_empty_execution_payload, ) from eth2spec.test.helpers.keys import privkeys +from eth2spec.test.helpers.multi_operations import ( + get_random_consolidation_requests, + get_random_deposit_requests, + get_random_withdrawal_requests, +) def run_execution_payload_processing( @@ -92,18 +99,10 @@ def prepare_execution_payload_envelope( execution_payload = build_empty_execution_payload(spec, state) if execution_requests is None: - execution_requests = spec.ExecutionRequests( - deposits=spec.List[spec.DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD](), - withdrawals=spec.List[ - spec.WithdrawalRequest, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD - ](), - consolidations=spec.List[ - spec.ConsolidationRequest, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD - ](), - ) + execution_requests = spec.ExecutionRequests() if blob_kzg_commitments is None: - blob_kzg_commitments = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]() + blob_kzg_commitments = spec.ProgressiveList[spec.KZGCommitment]() # Create a copy of state for computing state_root after execution payload processing if state_root is None: @@ -181,7 +180,7 @@ def setup_state_with_payload_bid(spec, state, builder_index=None, value=None): value = spec.Gwei(0) # Create and set the latest execution payload bid - kzg_list = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]() + kzg_list = spec.ProgressiveList[spec.KZGCommitment]() bid = spec.ExecutionPayloadBid( parent_block_hash=state.latest_block_hash, parent_block_root=state.latest_block_header.hash_tree_root(), @@ -196,7 +195,7 @@ def setup_state_with_payload_bid(spec, state, builder_index=None, value=None): state.latest_execution_payload_bid = bid # Setup withdrawals root - empty_withdrawals = spec.List[spec.Withdrawal, spec.MAX_WITHDRAWALS_PER_PAYLOAD]() + empty_withdrawals = spec.ProgressiveList[spec.Withdrawal]() state.latest_withdrawals_root = empty_withdrawals.hash_tree_root() # Add pending payment if value > 0 @@ -399,7 +398,7 @@ def test_process_execution_payload_with_blob_commitments(spec, state): setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(3000000)) # Create blob commitments - blob_kzg_commitments = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( + blob_kzg_commitments = spec.ProgressiveList[spec.KZGCommitment]( [ spec.KZGCommitment(b"\x42" * 48), spec.KZGCommitment(b"\x43" * 48), @@ -474,7 +473,7 @@ def test_process_execution_payload_with_execution_requests(spec, state): # Create execution requests execution_requests = spec.ExecutionRequests( - deposits=spec.List[spec.DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]( + deposits=spec.DepositRequests( [ spec.DepositRequest( pubkey=spec.BLSPubkey(b"\x01" * 48), @@ -485,7 +484,7 @@ def test_process_execution_payload_with_execution_requests(spec, state): ) ] ), - withdrawals=spec.List[spec.WithdrawalRequest, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD]( + withdrawals=spec.WithdrawalRequests( [ spec.WithdrawalRequest( source_address=spec.ExecutionAddress(b"\x04" * 20), @@ -494,9 +493,7 @@ def test_process_execution_payload_with_execution_requests(spec, state): ) ] ), - consolidations=spec.List[ - spec.ConsolidationRequest, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD - ]( + consolidations=spec.ConsolidationRequests( [ spec.ConsolidationRequest( source_address=spec.ExecutionAddress(b"\x06" * 20), @@ -562,6 +559,78 @@ def test_process_execution_payload_with_execution_requests(spec, state): assert cleared_payment.withdrawal.builder_index == empty_payment.withdrawal.builder_index +def run_execution_payload_with_invalid_execution_requests_test(spec, state, execution_requests): + """ + Test execution payload processing with invalid execution requests + """ + proposer_index = spec.get_beacon_proposer_index(state) + # Use a different validator as builder + builder_index = (proposer_index + 3) % len(state.validators) + make_validator_builder(spec, state, builder_index) + + setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(4000000)) + + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.block_hash = state.latest_execution_payload_bid.block_hash + execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit + execution_payload.parent_hash = state.latest_block_hash + + signed_envelope = prepare_execution_payload_envelope( + spec, + state, + builder_index=builder_index, + execution_payload=execution_payload, + execution_requests=execution_requests, + ) + + yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) + + +@with_gloas_and_later +@spec_state_test +def test_process_execution_payload_too_many_deposit_requests(spec, state): + rng = Random(3456) + yield from run_execution_payload_with_invalid_execution_requests_test( + spec, + state, + spec.ExecutionRequests( + deposits=get_random_deposit_requests( + spec, state, rng, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD + 1 + ) + ), + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_execution_payload_too_many_withdrawal_requests(spec, state): + rng = Random(3456) + yield from run_execution_payload_with_invalid_execution_requests_test( + spec, + state, + spec.ExecutionRequests( + withdrawals=get_random_withdrawal_requests( + spec, state, rng, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD + 1 + ) + ), + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_execution_payload_too_many_consolidation_requests(spec, state): + rng = Random(3456) + yield from run_execution_payload_with_invalid_execution_requests_test( + spec, + state, + spec.ExecutionRequests( + consolidations=get_random_consolidation_requests( + spec, state, rng, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD + 1 + ) + ), + ) + + # # Invalid signature tests # @@ -702,7 +771,7 @@ def test_process_execution_payload_wrong_blob_commitments_root(spec, state): make_validator_builder(spec, state, builder_index) setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(2800000)) - original_blob_commitments = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( + original_blob_commitments = spec.ProgressiveList[spec.KZGCommitment]( [spec.KZGCommitment(b"\x11" * 48)] ) state.latest_execution_payload_bid.blob_kzg_commitments_root = ( @@ -715,7 +784,7 @@ def test_process_execution_payload_wrong_blob_commitments_root(spec, state): execution_payload.parent_hash = state.latest_block_hash # Use different blob commitments - wrong_blob_commitments = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( + wrong_blob_commitments = spec.ProgressiveList[spec.KZGCommitment]( [spec.KZGCommitment(b"\x22" * 48)] ) @@ -887,9 +956,7 @@ def test_process_execution_payload_max_blob_commitments_valid(spec, state): max_blob_commitments = [ spec.KZGCommitment(b"\x42" * 48) for _ in range(spec.config.MAX_BLOBS_PER_BLOCK) ] - blob_kzg_commitments = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( - max_blob_commitments - ) + blob_kzg_commitments = spec.ProgressiveList[spec.KZGCommitment](max_blob_commitments) # Update committed bid to match state.latest_execution_payload_bid.blob_kzg_commitments_root = ( diff --git a/tests/core/pyspec/eth2spec/test/gloas/block_processing/test_process_execution_payload_bid.py b/tests/core/pyspec/eth2spec/test/gloas/block_processing/test_process_execution_payload_bid.py index a70dae5547..24ba2b7a45 100644 --- a/tests/core/pyspec/eth2spec/test/gloas/block_processing/test_process_execution_payload_bid.py +++ b/tests/core/pyspec/eth2spec/test/gloas/block_processing/test_process_execution_payload_bid.py @@ -85,7 +85,7 @@ def prepare_signed_execution_payload_bid( raise ValueError("Self-builder (builder_index == proposer_index) must use zero value") if blob_kzg_commitments_root is None: - kzg_list = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]() + kzg_list = spec.ProgressiveList[spec.KZGCommitment]() blob_kzg_commitments_root = kzg_list.hash_tree_root() bid = spec.ExecutionPayloadBid( @@ -355,7 +355,7 @@ def test_process_execution_payload_bid_self_build_non_zero_value(spec, state): Test self-builder with non-zero value fails (builder_index == proposer_index but value > 0) """ block = build_empty_block_for_next_slot(spec, state) - kzg_list = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]() + kzg_list = spec.ProgressiveList[spec.KZGCommitment]() blob_kzg_commitments_root = kzg_list.hash_tree_root() bid = spec.ExecutionPayloadBid( diff --git a/tests/core/pyspec/eth2spec/test/gloas/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/gloas/sanity/test_blocks.py new file mode 100644 index 0000000000..974216f631 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/gloas/sanity/test_blocks.py @@ -0,0 +1,164 @@ +from random import Random + +from eth2spec.test.context import ( + spec_state_test, + with_gloas_and_later, +) +from eth2spec.test.helpers.attestations import get_max_attestations +from eth2spec.test.helpers.attester_slashings import ( + get_max_attester_slashings, + get_valid_attester_slashing_by_indices, +) +from eth2spec.test.helpers.block import ( + build_empty_block_for_next_slot, +) +from eth2spec.test.helpers.bls_to_execution_changes import get_signed_address_change +from eth2spec.test.helpers.deposits import build_deposit_data, deposit_from_context +from eth2spec.test.helpers.keys import privkeys, pubkeys +from eth2spec.test.helpers.multi_operations import ( + get_random_attestations, +) +from eth2spec.test.helpers.proposer_slashings import ( + get_valid_proposer_slashings, +) +from eth2spec.test.helpers.state import next_epoch, next_slots, state_transition_and_sign_block +from eth2spec.test.helpers.voluntary_exits import prepare_signed_exits + + +@with_gloas_and_later +@spec_state_test +def test_invalid_too_many_proposer_slashings(spec, state): + num_slashings = spec.MAX_PROPOSER_SLASHINGS + 1 + proposer_slashings = get_valid_proposer_slashings(spec, state, num_slashings) + + yield "pre", state + + block = build_empty_block_for_next_slot(spec, state) + block.body.proposer_slashings = proposer_slashings + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield "blocks", [signed_block] + yield "post", None + + +@with_gloas_and_later +@spec_state_test +def test_invalid_too_many_attester_slashings(spec, state): + num_slashings = get_max_attester_slashings(spec) + 1 + full_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[:8] + per_slashing_length = len(full_indices) // num_slashings + attester_slashings = [ + get_valid_attester_slashing_by_indices( + spec, + state, + full_indices[i * per_slashing_length : (i + 1) * per_slashing_length], + signed_1=True, + signed_2=True, + ) + for i in range(num_slashings) + ] + + yield "pre", state + + block = build_empty_block_for_next_slot(spec, state) + block.body.attester_slashings = attester_slashings + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield "blocks", [signed_block] + yield "post", None + + +@with_gloas_and_later +@spec_state_test +def test_invalid_too_many_attestations(spec, state): + rng = Random(2000) + + next_epoch(spec, state) + num_attestations = get_max_attestations(spec) + 1 + attestations = get_random_attestations(spec, state, rng, num_attestations) + + yield "pre", state + + block = build_empty_block_for_next_slot(spec, state) + block.body.attestations = attestations + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield "blocks", [signed_block] + yield "post", None + + +@with_gloas_and_later +@spec_state_test +def test_invalid_too_many_deposits(spec, state): + num_deposits = spec.MAX_DEPOSITS + 1 + validator_index = len(state.validators) + amount = spec.MIN_ACTIVATION_BALANCE + + deposit_data_list = [spec.DepositData() for _ in range(state.eth1_deposit_index)] + for _ in range(num_deposits): + deposit_data = build_deposit_data( + spec, + pubkeys[validator_index], + privkeys[validator_index], + amount, + withdrawal_credentials=b"\x00" * 32, + signed=True, + ) + deposit_data_list.append(deposit_data) + + deposits = [] + deposit_root = None + for i in range(state.eth1_deposit_index, state.eth1_deposit_index + num_deposits): + deposit, deposit_root, _ = deposit_from_context(spec, deposit_data_list, i) + deposits.append(deposit) + + state.eth1_data.deposit_root = deposit_root + state.eth1_data.deposit_count = len(deposit_data_list) + + yield "pre", state + + block = build_empty_block_for_next_slot(spec, state) + block.body.deposits = deposits + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield "blocks", [signed_block] + yield "post", None + + +@with_gloas_and_later +@spec_state_test +def test_invalid_too_many_voluntary_exits(spec, state): + next_slots(spec, state, spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH) + num_exits = spec.MAX_VOLUNTARY_EXITS + 1 + full_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[ + :num_exits + ] + signed_exits = prepare_signed_exits(spec, state, full_indices) + + yield "pre", state + + block = build_empty_block_for_next_slot(spec, state) + block.body.voluntary_exits = signed_exits + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield "blocks", [signed_block] + yield "post", None + + +@with_gloas_and_later +@spec_state_test +def test_invalid_too_many_bls_to_execution_changes(spec, state): + num_address_changes = spec.MAX_BLS_TO_EXECUTION_CHANGES + 1 + signed_address_changes = [ + get_signed_address_change(spec, state, validator_index=i) + for i in range(num_address_changes) + ] + + yield "pre", state + + block = build_empty_block_for_next_slot(spec, state) + block.body.bls_to_execution_changes = signed_address_changes + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield "blocks", [signed_block] + yield "post", None diff --git a/tests/core/pyspec/eth2spec/test/helpers/attestations.py b/tests/core/pyspec/eth2spec/test/helpers/attestations.py index a74684c2f5..42c2bbe3ff 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/attestations.py +++ b/tests/core/pyspec/eth2spec/test/helpers/attestations.py @@ -493,9 +493,7 @@ def get_empty_eip7549_aggregation_bits(spec, state, committee_bits, slot): for index in committee_indices: committee = spec.get_beacon_committee(state, slot, index) participants_count += len(committee) - aggregation_bits = Bitlist[spec.MAX_VALIDATORS_PER_COMMITTEE * spec.MAX_COMMITTEES_PER_SLOT]( - [False] * participants_count - ) + aggregation_bits = spec.AggregationBits([False] * participants_count) return aggregation_bits @@ -512,3 +510,24 @@ def get_eip7549_aggregation_bits_offset(spec, state, slot, committee_bits, commi committee = spec.get_beacon_committee(state, slot, committee_indices[i]) offset += len(committee) return offset + + +def needs_upgrade_to_gloas(spec, new_spec): + return is_post_gloas(new_spec) and not is_post_gloas(spec) + + +def check_attestation_equal(spec, new_spec, data, upgraded): + assert list(data.aggregation_bits) == list(upgraded.aggregation_bits) + assert data.data == upgraded.data + assert data.signature == upgraded.signature + assert list(data.committee_bits) == list(upgraded.committee_bits) + + +def upgrade_attestation_to_new_spec(spec, new_spec, data): + upgraded = data + + if needs_upgrade_to_gloas(spec, new_spec): + upgraded = new_spec.upgrade_attestation_to_gloas(upgraded) + check_attestation_equal(spec, new_spec, data, upgraded) + + return upgraded diff --git a/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py b/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py index 9c2568a4d1..4a9f349e27 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py +++ b/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py @@ -3,7 +3,7 @@ sign_attestation, sign_indexed_attestation, ) -from eth2spec.test.helpers.forks import is_post_electra +from eth2spec.test.helpers.forks import is_post_electra, is_post_gloas def get_valid_attester_slashing( @@ -74,3 +74,38 @@ def get_max_attester_slashings(spec): return spec.MAX_ATTESTER_SLASHINGS_ELECTRA else: return spec.MAX_ATTESTER_SLASHINGS + + +def needs_upgrade_to_gloas(spec, new_spec): + return is_post_gloas(new_spec) and not is_post_gloas(spec) + + +def check_indexed_attestation_equal(spec, new_spec, data, upgraded): + assert list(data.attesting_indices) == list(upgraded.attesting_indices) + assert data.data == upgraded.data + assert data.signature == upgraded.signature + + +def upgrade_indexed_attestation_to_new_spec(spec, new_spec, data): + upgraded = data + + if needs_upgrade_to_gloas(spec, new_spec): + upgraded = new_spec.upgrade_indexed_attestation_to_gloas(upgraded) + check_indexed_attestation_equal(spec, new_spec, data, upgraded) + + return upgraded + + +def check_attester_slashing_equal(spec, new_spec, data, upgraded): + check_indexed_attestation_equal(spec, new_spec, data.attestation_1, upgraded.attestation_1) + check_indexed_attestation_equal(spec, new_spec, data.attestation_2, upgraded.attestation_2) + + +def upgrade_attester_slashing_to_new_spec(spec, new_spec, data): + upgraded = data + + if needs_upgrade_to_gloas(spec, new_spec): + upgraded = new_spec.upgrade_attester_slashing_to_gloas(upgraded) + check_attester_slashing_equal(spec, new_spec, data, upgraded) + + return upgraded diff --git a/tests/core/pyspec/eth2spec/test/helpers/blob.py b/tests/core/pyspec/eth2spec/test/helpers/blob.py index 2b4fd1e8d9..6b4c5c645e 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/blob.py +++ b/tests/core/pyspec/eth2spec/test/helpers/blob.py @@ -145,9 +145,7 @@ def get_block_with_blob(spec, state, rng: Random | None = None, blob_count=1): spec, blob_count=blob_count, rng=rng or random.Random(5566) ) if is_post_gloas(spec): - blob_kzg_commitments = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( - blob_kzg_commitments - ) + blob_kzg_commitments = spec.ProgressiveList[spec.KZGCommitment](blob_kzg_commitments) kzg_root = blob_kzg_commitments.hash_tree_root() block.body.signed_execution_payload_bid.message.blob_kzg_commitments_root = kzg_root # For self-builds, use point at infinity signature as per spec diff --git a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py index b0c9c4f62a..9bdba84567 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py @@ -79,7 +79,7 @@ def get_execution_payload_bid(spec, state, execution_payload): raise ValueError("get_execution_payload_bid only available for gloas and later") parent_block_root = hash_tree_root(state.latest_block_header) - kzg_list = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]() + kzg_list = spec.ProgressiveList[spec.KZGCommitment]() builder_index = spec.get_beacon_proposer_index(state) return spec.ExecutionPayloadBid( @@ -316,7 +316,7 @@ def build_empty_post_gloas_execution_payload_bid(spec, state): if not is_post_gloas(spec): return parent_block_root = hash_tree_root(state.latest_block_header) - kzg_list = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]() + kzg_list = spec.ProgressiveList[spec.KZGCommitment]() # Use self-build: builder_index is the same as the beacon proposer index builder_index = spec.get_beacon_proposer_index(state) # Set block_hash to a different value than spec.Hash32(), @@ -434,7 +434,7 @@ def build_randomized_execution_payload(spec, state, rng): def build_state_with_incomplete_transition(spec, state): if is_post_gloas(spec): # In Gloas, we need to set up the execution payload bid instead - kzgs = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]() + kzgs = spec.ProgressiveList[spec.KZGCommitment]() bid = spec.ExecutionPayloadBid( slot=state.slot, value=spec.Gwei(0), diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py b/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py index a55e0ad3ae..3a6037421a 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py @@ -6,6 +6,7 @@ ) from eth2spec.test.helpers.attester_slashings import ( get_valid_attester_slashing_by_indices, + upgrade_attester_slashing_to_new_spec, ) from eth2spec.test.helpers.block import ( build_empty_block, @@ -382,6 +383,10 @@ def run_transition_with_operation( signed_1=True, signed_2=True, ) + if is_at_fork: + attester_slashing = upgrade_attester_slashing_to_new_spec( + target_spec, post_spec, attester_slashing + ) operation_dict = {"attester_slashings": [attester_slashing]} elif operation_type == OperationType.DEPOSIT: # create a new deposit diff --git a/tests/core/pyspec/eth2spec/test/helpers/genesis.py b/tests/core/pyspec/eth2spec/test/helpers/genesis.py index fb2def8818..caa89be219 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/genesis.py +++ b/tests/core/pyspec/eth2spec/test/helpers/genesis.py @@ -232,7 +232,7 @@ def create_genesis_state(spec, validator_balances, activation_threshold): if is_post_gloas(spec): state.execution_payload_availability = [0b1 for _ in range(spec.SLOTS_PER_HISTORICAL_ROOT)] - withdrawals = spec.List[spec.Withdrawal, spec.MAX_WITHDRAWALS_PER_PAYLOAD]() + withdrawals = spec.ProgressiveList[spec.Withdrawal]() state.latest_withdrawals_root = withdrawals.hash_tree_root() state.builder_pending_payments = [ spec.BuilderPendingPayment() for _ in range(2 * spec.SLOTS_PER_EPOCH) diff --git a/tests/core/pyspec/eth2spec/test/helpers/keys.py b/tests/core/pyspec/eth2spec/test/helpers/keys.py index a849f11320..8fcc2230d3 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/keys.py +++ b/tests/core/pyspec/eth2spec/test/helpers/keys.py @@ -1,7 +1,7 @@ from py_ecc.bls import G2ProofOfPossession as bls # Enough keys for 256 validators per slot in worst-case epoch length -privkeys = [i + 1 for i in range(32 * 256)] +privkeys = [i + 1 for i in range(32 * 256 + 1)] pubkeys = [bls.SkToPk(privkey) for privkey in privkeys] pubkey_to_privkey = {pubkey: privkey for privkey, pubkey in zip(privkeys, pubkeys)} diff --git a/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py b/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py index 8eed4db020..54f605174d 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py +++ b/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py @@ -107,8 +107,9 @@ def get_random_attester_slashings(spec, state, rng, slashed_indices=[]): return slashings -def get_random_attestations(spec, state, rng): - num_attestations = rng.randrange(1, get_max_attestations(spec)) +def get_random_attestations(spec, state, rng, num_attestations=None): + if num_attestations is None: + num_attestations = rng.randrange(1, get_max_attestations(spec)) attestations = [ get_valid_attestation( diff --git a/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py b/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py index fa6d4c5248..58470f0ee5 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py +++ b/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py @@ -134,3 +134,16 @@ def get_valid_proposer_slashing( signed_header_1=signed_header_1, signed_header_2=signed_header_2, ) + + +def get_valid_proposer_slashings(spec, state, num_slashings): + proposer_slashings = [] + for i in range(num_slashings): + slashed_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[i] + assert not state.validators[slashed_index].slashed + + proposer_slashing = get_valid_proposer_slashing( + spec, state, slashed_index=slashed_index, signed_1=True, signed_2=True + ) + proposer_slashings.append(proposer_slashing) + return proposer_slashings diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py index c3572930e7..09d8f47416 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -2,7 +2,6 @@ from lru import LRU -from eth2spec.phase0.mainnet import VALIDATOR_REGISTRY_LIMIT # equal everywhere, fine to import from eth2spec.test.helpers.attestations import ( cached_prepare_state_with_attestations, ) @@ -16,12 +15,12 @@ from eth2spec.test.helpers.state import ( next_epoch, ) -from eth2spec.utils.ssz.ssz_typing import Container, List, uint64 +from eth2spec.utils.ssz.ssz_typing import Container, ProgressiveList, uint64 class Deltas(Container): - rewards: List[uint64, VALIDATOR_REGISTRY_LIMIT] - penalties: List[uint64, VALIDATOR_REGISTRY_LIMIT] + rewards: ProgressiveList[uint64] + penalties: ProgressiveList[uint64] def get_inactivity_penalty_quotient(spec): diff --git a/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_attester_slashing.py b/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_attester_slashing.py index 70799da218..e981c8db45 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_attester_slashing.py +++ b/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_attester_slashing.py @@ -10,6 +10,8 @@ spec_test, with_all_phases, with_custom_state, + with_gloas_and_later, + with_presets, ) from eth2spec.test.helpers.attestations import sign_indexed_attestation from eth2spec.test.helpers.attester_slashings import ( @@ -19,6 +21,7 @@ get_valid_attester_slashing, get_valid_attester_slashing_by_indices, ) +from eth2spec.test.helpers.constants import MINIMAL from eth2spec.test.helpers.proposer_slashings import ( get_min_slashing_penalty_quotient, get_whistleblower_reward_quotient, @@ -528,3 +531,26 @@ def test_invalid_unsorted_att_2(spec, state): sign_indexed_attestation(spec, state, attester_slashing.attestation_2) yield from run_attester_slashing_processing(spec, state, attester_slashing, valid=False) + + +@with_gloas_and_later +@with_presets( + [MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated", +) +@spec_test +@with_custom_state( + balances_fn=lambda spec: [spec.MAX_EFFECTIVE_BALANCE] + * (spec.MAX_VALIDATORS_PER_COMMITTEE * spec.MAX_COMMITTEES_PER_SLOT + 1), + threshold_fn=lambda spec: spec.config.EJECTION_BALANCE, +) +@single_phase +def test_invalid_too_many_attesting_indices(spec, state): + indices = [ + spec.ValidatorIndex(i) + for i in range(spec.MAX_VALIDATORS_PER_COMMITTEE * spec.MAX_COMMITTEES_PER_SLOT + 1) + ] + attester_slashing = get_valid_attester_slashing_by_indices( + spec, state, indices, signed_1=True, signed_2=True + ) + yield from run_attester_slashing_processing(spec, state, attester_slashing, valid=False) diff --git a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py index 2cf383d952..346ba03a9e 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py @@ -50,6 +50,7 @@ from eth2spec.test.helpers.proposer_slashings import ( check_proposer_slashing_effect, get_valid_proposer_slashing, + get_valid_proposer_slashings, ) from eth2spec.test.helpers.state import ( get_balance, @@ -544,15 +545,7 @@ def test_multiple_different_proposer_slashings_same_block(spec, state): pre_state = state.copy() num_slashings = 3 - proposer_slashings = [] - for i in range(num_slashings): - slashed_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[i] - assert not state.validators[slashed_index].slashed - - proposer_slashing = get_valid_proposer_slashing( - spec, state, slashed_index=slashed_index, signed_1=True, signed_2=True - ) - proposer_slashings.append(proposer_slashing) + proposer_slashings = get_valid_proposer_slashings(spec, state, num_slashings) yield "pre", state diff --git a/tests/core/pyspec/eth2spec/test/phase0/ssz_static/test_ssz_static.py b/tests/core/pyspec/eth2spec/test/phase0/ssz_static/test_ssz_static.py index 11f1e4939a..03a48f2133 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/ssz_static/test_ssz_static.py +++ b/tests/core/pyspec/eth2spec/test/phase0/ssz_static/test_ssz_static.py @@ -16,7 +16,7 @@ hash_tree_root, serialize, ) -from eth2spec.utils.ssz.ssz_typing import Container +from eth2spec.utils.ssz.ssz_typing import Container, ProgressiveContainer from tests.infra.manifest import Manifest, manifest from tests.infra.template_test import template_test @@ -109,8 +109,10 @@ def _get_spec_ssz_types_names(spec: str) -> list[str]: return [ name for (name, value) in getmembers(spec, isclass) - if issubclass(value, Container) - and value != Container # only the subclasses, not the imported base class + if issubclass(value, Container | ProgressiveContainer) + # only the subclasses, not the imported base class + and value != Container + and value != ProgressiveContainer ] def _get_ssz_types_to_specs_mapping() -> dict[str, list[str]]: diff --git a/tests/infra/test_md_to_spec.py b/tests/infra/test_md_to_spec.py index 33d68b305b..5b219a2d03 100644 --- a/tests/infra/test_md_to_spec.py +++ b/tests/infra/test_md_to_spec.py @@ -2,6 +2,7 @@ import pytest +from pysetup.helpers import finalized_spec_object from pysetup.md_to_spec import MarkdownToSpec from pysetup.typing import SpecObject @@ -35,7 +36,6 @@ def test_constructor_initializes_fields(dummy_file, dummy_preset, dummy_config): assert m2s.config == dummy_config assert m2s.preset_name == preset_name assert isinstance(m2s.spec, dict) - assert isinstance(m2s.all_custom_types, dict) assert hasattr(m2s, "document_iterator") assert m2s.current_heading_name is None @@ -313,7 +313,7 @@ class PayloadAttributes(object): assert "PayloadAttributes" not in spec_obj.dataclasses -def test_finalize_types_called_and_updates_custom_types( +def test_finalized_spec_object_updates_custom_types( tmp_path, dummy_preset, dummy_config, monkeypatch ): # Minimal markdown with a type definition @@ -324,6 +324,11 @@ def test_finalize_types_called_and_updates_custom_types( | ---------------- | -------------- | --------------------------------- | | `Slot` | `uint64` | a slot number | | `Epoch` | `uint64` | an epoch number | + +| Name | Value | +| --------------- | ---------- | +| `GENESIS_SLOT` | `Slot(0)` | +| `GENESIS_EPOCH` | `Epoch(0)` | """ file = tmp_path / "types.md" file.write_text(md_content) @@ -334,18 +339,7 @@ def test_finalize_types_called_and_updates_custom_types( preset_name="mainnet", ) - # Spy on _finalize_types - called = {} - orig_finalize_types = m2s._finalize_types - - def spy_finalize_types(): - called["ran"] = True - return orig_finalize_types() - - monkeypatch.setattr(m2s, "_finalize_types", spy_finalize_types) - - spec_obj = m2s.run() - assert called.get("ran") is True - # After _finalize_types, custom_types should include 'Slot' and 'Epoch' + spec_obj = finalized_spec_object(m2s.run()) + # After finalized_spec_object, custom_types should include 'Slot' and 'Epoch' assert spec_obj.custom_types["Slot"] == "uint64" assert spec_obj.custom_types["Epoch"] == "uint64" diff --git a/uv.lock b/uv.lock index 09a499bdd8..f3c43fb83d 100644 --- a/uv.lock +++ b/uv.lock @@ -592,7 +592,7 @@ requires-dist = [ { name = "pytest-html", marker = "extra == 'test'", specifier = "==4.1.1" }, { name = "pytest-xdist", marker = "extra == 'test'", specifier = "==3.8.0" }, { name = "python-snappy", marker = "extra == 'generator'", specifier = "==0.7.3" }, - { name = "remerkleable", git = "https://github.com/ethereum/remerkleable?rev=92dcbb6f0507035d6875986fc263a05fec19d473" }, + { name = "remerkleable", git = "https://github.com/ethereum/remerkleable?rev=71a94389375aa9afbe39145dcd26d4cddafa140d" }, { name = "rich", marker = "extra == 'generator'", specifier = "==14.1.0" }, { name = "ruamel-yaml", specifier = "==0.18.15" }, { name = "ruff", marker = "extra == 'lint'", specifier = "==0.13.2" }, @@ -1709,7 +1709,7 @@ wheels = [ [[package]] name = "remerkleable" version = "0.1.28" -source = { git = "https://github.com/ethereum/remerkleable?rev=92dcbb6f0507035d6875986fc263a05fec19d473#92dcbb6f0507035d6875986fc263a05fec19d473" } +source = { git = "https://github.com/ethereum/remerkleable?rev=71a94389375aa9afbe39145dcd26d4cddafa140d#71a94389375aa9afbe39145dcd26d4cddafa140d" } [[package]] name = "requests" From 4605657775d0211f1fe224e492ff4f9d73659518 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Wed, 8 Oct 2025 16:54:28 +0200 Subject: [PATCH 2/2] Update EIP reference comments --- specs/gloas/beacon-chain.md | 6 ++++++ ssz/simple-serialize.md | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index bb61c2fce5..6b9e237252 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -300,14 +300,20 @@ class BeaconBlockBody(ProgressiveContainer(active_fields=[1] * 12)): randao_reveal: BLSSignature eth1_data: Eth1Data graffiti: Bytes32 + # [Modified in Gloas:EIP7688] proposer_slashings: ProgressiveList[ProposerSlashing] + # [Modified in Gloas:EIP7688] attester_slashings: ProgressiveList[AttesterSlashing] + # [Modified in Gloas:EIP7688] attestations: ProgressiveList[Attestation] + # [Modified in Gloas:EIP7688] deposits: ProgressiveList[Deposit] + # [Modified in Gloas:EIP7688] voluntary_exits: ProgressiveList[SignedVoluntaryExit] sync_aggregate: SyncAggregate # [Modified in Gloas:EIP7732] # Removed `execution_payload` + # [Modified in Gloas:EIP7688] bls_to_execution_changes: ProgressiveList[SignedBLSToExecutionChange] # [Modified in Gloas:EIP7732] # Removed `blob_kzg_commitments` diff --git a/ssz/simple-serialize.md b/ssz/simple-serialize.md index bbd8f93ea4..1b0affa345 100644 --- a/ssz/simple-serialize.md +++ b/ssz/simple-serialize.md @@ -55,7 +55,7 @@ foo: uint64 bar: boolean ``` -- **progressive container** _[EIP-7495, currently unused]_: ordered +- **progressive container** _[EIP-7495]_: ordered heterogeneous collection of values with stable Merkleization - python dataclass notation with key-type pairs, e.g. ```python @@ -73,7 +73,7 @@ - **list**: ordered variable-length homogeneous collection, limited to `N` values - notation `List[type, N]`, e.g. `List[uint64, N]` -- **progressive list** _[EIP-7916, currently unused]_: ordered variable-length +- **progressive list** _[EIP-7916]_: ordered variable-length homogeneous collection, without limit - notation `ProgressiveList[type]`, e.g. `ProgressiveList[uint64]` - **bitvector**: ordered fixed-length collection of `boolean` values, with `N` @@ -82,7 +82,7 @@ - **bitlist**: ordered variable-length collection of `boolean` values, limited to `N` bits - notation `Bitlist[N]` -- **progressive bitlist** _[EIP-7916, currently unused]_: ordered +- **progressive bitlist** _[EIP-7916]_: ordered variable-length collection of `boolean` values, without limit - notation `ProgressiveBitlist` - **union**: union type containing one of the given subtypes