Skip to content

Commit 457cfbf

Browse files
committed
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: ethereum/pm#1356 (comment)
1 parent 2a0ac6a commit 457cfbf

36 files changed

+755
-197
lines changed

presets/mainnet/gloas.yaml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,3 @@ PTC_SIZE: 512
99
# ---------------------------------------------------------------
1010
# 2**2 (= 4) attestations
1111
MAX_PAYLOAD_ATTESTATIONS: 4
12-
13-
# State list lengths
14-
# ---------------------------------------------------------------
15-
# 2**20 (= 1,048,576) builder pending withdrawals
16-
BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576

presets/minimal/gloas.yaml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,3 @@ PTC_SIZE: 2
99
# ---------------------------------------------------------------
1010
# 2**2 (= 4) attestations
1111
MAX_PAYLOAD_ATTESTATIONS: 4
12-
13-
# State list lengths
14-
# ---------------------------------------------------------------
15-
# 2**20 (= 1,048,576) builder pending withdrawals
16-
BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ dependencies = [
2020
"py_arkworks_bls12381==0.3.8",
2121
"py_ecc==8.0.0",
2222
"pycryptodome==3.23.0",
23-
"remerkleable @ git+https://github.com/ethereum/remerkleable@92dcbb6f0507035d6875986fc263a05fec19d473",
23+
"remerkleable @ git+https://github.com/ethereum/remerkleable@71a94389375aa9afbe39145dcd26d4cddafa140d",
2424
"ruamel.yaml==0.18.15",
2525
"setuptools==80.9.0",
2626
"trie==3.1.0",

pysetup/generate_specs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from pysetup.helpers import (
4444
combine_spec_objects,
4545
dependency_order_class_objects,
46+
finalized_spec_object,
4647
objects_to_spec,
4748
parse_config_vars,
4849
)
@@ -118,6 +119,7 @@ def build_spec(
118119
spec_object = all_specs[0]
119120
for value in all_specs[1:]:
120121
spec_object = combine_spec_objects(spec_object, value)
122+
spec_object = finalized_spec_object(spec_object)
121123

122124
class_objects = {**spec_object.ssz_objects, **spec_object.dataclasses}
123125

@@ -127,7 +129,7 @@ def build_spec(
127129
new_objects = copy.deepcopy(class_objects)
128130
dependency_order_class_objects(
129131
class_objects,
130-
spec_object.custom_types | spec_object.preset_dep_custom_types,
132+
spec_object.custom_types,
131133
)
132134

133135
return objects_to_spec(preset_name, spec_object, fork, class_objects)

pysetup/helpers.py

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,19 @@ def collect_prev_forks(fork: str) -> list[str]:
2323

2424

2525
def requires_mypy_type_ignore(value: str) -> bool:
26-
return value.startswith("ByteVector") or (
27-
value.startswith("Vector") and any(k in value for k in ["ceillog2", "floorlog2"])
26+
return (
27+
value.startswith("Bitlist")
28+
or value.startswith("ByteVector")
29+
or (value.startswith("List") and not re.match(r"^List\[\w+,\s*\w+\]$", value))
30+
or (value.startswith("Vector") and any(k in value for k in ["ceillog2", "floorlog2"]))
31+
)
32+
33+
34+
def gen_new_type_definition(name: str, value: str) -> str:
35+
return (
36+
f"class {name}({value}):\n pass"
37+
if not requires_mypy_type_ignore(value)
38+
else f"class {name}(\n {value} # type: ignore\n):\n pass"
2839
)
2940

3041

@@ -41,19 +52,11 @@ def objects_to_spec(
4152
"""
4253

4354
def gen_new_type_definitions(custom_types: dict[str, str]) -> str:
44-
return "\n\n".join(
45-
[
46-
(
47-
f"class {key}({value}):\n pass\n"
48-
if not requires_mypy_type_ignore(value)
49-
else f"class {key}({value}): # type: ignore\n pass\n"
50-
)
51-
for key, value in custom_types.items()
52-
]
55+
return "\n\n\n".join(
56+
[gen_new_type_definition(key, value) for key, value in custom_types.items()]
5357
)
5458

5559
new_type_definitions = gen_new_type_definitions(spec_object.custom_types)
56-
preset_dep_new_type_definitions = gen_new_type_definitions(spec_object.preset_dep_custom_types)
5760

5861
# Collect builders with the reversed previous forks
5962
# e.g. `[bellatrix, altair, phase0]` -> `[phase0, altair, bellatrix]`
@@ -224,10 +227,9 @@ def format_constant(name: str, vardef: VariableDefinition) -> str:
224227
ssz_dep_constants,
225228
new_type_definitions,
226229
constant_vars_spec,
227-
# The presets that some SSZ types require. Need to be defined before `preset_dep_new_type_definitions`
230+
# The presets that some SSZ types require.
228231
preset_vars_spec,
229232
preset_dep_constant_vars_spec,
230-
preset_dep_new_type_definitions,
231233
config_spec,
232234
# Custom classes which are not required to be SSZ containers.
233235
classes,
@@ -267,8 +269,6 @@ def combine_dicts(old_dict: dict[str, T], new_dict: dict[str, T]) -> dict[str, T
267269
"bit",
268270
"Bitlist",
269271
"Bitvector",
270-
"BLSPubkey",
271-
"BLSSignature",
272272
"boolean",
273273
"byte",
274274
"ByteList",
@@ -292,6 +292,8 @@ def combine_dicts(old_dict: dict[str, T], new_dict: dict[str, T]) -> dict[str, T
292292
"floorlog2",
293293
"List",
294294
"Optional",
295+
"ProgressiveBitlist",
296+
"ProgressiveList",
295297
"Sequence",
296298
"Set",
297299
"Tuple",
@@ -312,10 +314,14 @@ def dependency_order_class_objects(objects: dict[str, str], custom_types: dict[s
312314
items = list(objects.items())
313315
for key, value in items:
314316
dependencies = []
315-
for line in value.split("\n"):
316-
if not re.match(r"\s+\w+: .+", line):
317+
for i, line in enumerate(value.split("\n")):
318+
if i == 0:
319+
match = re.match(r".+\((.+)\):", line)
320+
else:
321+
match = re.match(r"\s+\w+: (.+)", line)
322+
if not match:
317323
continue # skip whitespace etc.
318-
line = line[line.index(":") + 1 :] # strip of field name
324+
line = match.group(1)
319325
if "#" in line:
320326
line = line[: line.index("#")] # strip of comment
321327
dependencies.extend(
@@ -349,9 +355,6 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject:
349355
protocols = combine_protocols(spec0.protocols, spec1.protocols)
350356
functions = combine_dicts(spec0.functions, spec1.functions)
351357
custom_types = combine_dicts(spec0.custom_types, spec1.custom_types)
352-
preset_dep_custom_types = combine_dicts(
353-
spec0.preset_dep_custom_types, spec1.preset_dep_custom_types
354-
)
355358
constant_vars = combine_dicts(spec0.constant_vars, spec1.constant_vars)
356359
preset_dep_constant_vars = combine_dicts(
357360
spec0.preset_dep_constant_vars, spec1.preset_dep_constant_vars
@@ -366,7 +369,6 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject:
366369
functions=functions,
367370
protocols=protocols,
368371
custom_types=custom_types,
369-
preset_dep_custom_types=preset_dep_custom_types,
370372
constant_vars=constant_vars,
371373
preset_dep_constant_vars=preset_dep_constant_vars,
372374
preset_vars=preset_vars,
@@ -378,6 +380,41 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject:
378380
)
379381

380382

383+
def finalized_spec_object(spec_object: SpecObject) -> SpecObject:
384+
all_config_dependencies = {
385+
vardef.type_name or vardef.type_hint
386+
for vardef in (
387+
spec_object.constant_vars
388+
| spec_object.preset_dep_constant_vars
389+
| spec_object.preset_vars
390+
| spec_object.config_vars
391+
).values()
392+
if (vardef.type_name or vardef.type_hint) is not None
393+
}
394+
395+
custom_types = {}
396+
ssz_objects = spec_object.ssz_objects
397+
for name, value in spec_object.custom_types.items():
398+
if any(k in name for k in all_config_dependencies):
399+
custom_types[name] = value
400+
else:
401+
ssz_objects[name] = gen_new_type_definition(name, value)
402+
403+
return SpecObject(
404+
functions=spec_object.functions,
405+
protocols=spec_object.protocols,
406+
custom_types=custom_types,
407+
constant_vars=spec_object.constant_vars,
408+
preset_dep_constant_vars=spec_object.preset_dep_constant_vars,
409+
preset_vars=spec_object.preset_vars,
410+
config_vars=spec_object.config_vars,
411+
ssz_dep_constants=spec_object.ssz_dep_constants,
412+
func_dep_presets=spec_object.func_dep_presets,
413+
ssz_objects=ssz_objects,
414+
dataclasses=spec_object.dataclasses,
415+
)
416+
417+
381418
def parse_config_vars(conf: dict[str, str]) -> dict[str, str | list[dict[str, str]]]:
382419
"""
383420
Parses a dict of basic str/int/list types into a dict for insertion into the spec code.

pysetup/md_to_spec.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ def __init__(
3232
self.preset_name = preset_name
3333

3434
self.document_iterator: Iterator[Element] = self._parse_document(file_name)
35-
self.all_custom_types: dict[str, str] = {}
3635
self.current_heading_name: str | None = None
3736

3837
# Use a single dict to hold all SpecObject fields
@@ -44,7 +43,6 @@ def __init__(
4443
"func_dep_presets": {},
4544
"functions": {},
4645
"preset_dep_constant_vars": {},
47-
"preset_dep_custom_types": {},
4846
"preset_vars": {},
4947
"protocols": {},
5048
"ssz_dep_constants": {},
@@ -180,8 +178,12 @@ def _process_code_class(self, source: str, cls: ast.ClassDef) -> None:
180178
if class_name != self.current_heading_name:
181179
raise Exception(f"class_name {class_name} != current_name {self.current_heading_name}")
182180

183-
if parent_class:
184-
assert parent_class == "Container"
181+
if parent_class == "ProgressiveContainer":
182+
source = re.sub(
183+
r"^(.*ProgressiveContainer.*)$", r"\1 # type: ignore", source, flags=re.MULTILINE
184+
)
185+
else:
186+
assert parent_class is None or parent_class == "Container"
185187
self.spec["ssz_objects"][class_name] = source
186188

187189
def _process_table(self, table: Table) -> None:
@@ -202,9 +204,21 @@ def _process_table(self, table: Table) -> None:
202204
if not _is_constant_id(name):
203205
# Check for short type declarations
204206
if value.startswith(
205-
("uint", "Bytes", "ByteList", "Union", "Vector", "List", "ByteVector")
207+
(
208+
"uint",
209+
"Bitlist",
210+
"Bitvector",
211+
"ByteList",
212+
"ByteVector",
213+
"Bytes",
214+
"List",
215+
"ProgressiveBitlist",
216+
"ProgressiveList",
217+
"Union",
218+
"Vector",
219+
)
206220
):
207-
self.all_custom_types[name] = value
221+
self.spec["custom_types"][name] = value
208222
continue
209223

210224
# It is a constant name and a generalized index
@@ -418,7 +432,6 @@ def _process_html_block(self, html: HTMLBlock) -> None:
418432

419433
def _finalize_types(self) -> None:
420434
"""
421-
Processes all_custom_types into custom_types and preset_dep_custom_types.
422435
Calls helper functions to update KZG and CURDLEPROOFS setups if needed.
423436
"""
424437
# Update KZG trusted setup if needed
@@ -433,17 +446,6 @@ def _finalize_types(self) -> None:
433446
self.spec["constant_vars"], self.preset_name
434447
)
435448

436-
# Split all_custom_types into custom_types and preset_dep_custom_types
437-
self.spec["custom_types"] = {}
438-
self.spec["preset_dep_custom_types"] = {}
439-
for name, value in self.all_custom_types.items():
440-
if any(k in value for k in self.preset) or any(
441-
k in value for k in self.spec["preset_dep_constant_vars"]
442-
):
443-
self.spec["preset_dep_custom_types"][name] = value
444-
else:
445-
self.spec["custom_types"][name] = value
446-
447449
def _build_spec_object(self) -> SpecObject:
448450
"""
449451
Returns the SpecObject using all collected data.
@@ -456,7 +458,6 @@ def _build_spec_object(self) -> SpecObject:
456458
func_dep_presets=self.spec["func_dep_presets"],
457459
functions=self.spec["functions"],
458460
preset_dep_constant_vars=self.spec["preset_dep_constant_vars"],
459-
preset_dep_custom_types=self.spec["preset_dep_custom_types"],
460461
preset_vars=self.spec["preset_vars"],
461462
protocols=self.spec["protocols"],
462463
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]:
496497
parent_class = base.id
497498
elif isinstance(base, ast.Subscript):
498499
parent_class = base.value.id
500+
elif isinstance(base, ast.Call):
501+
parent_class = base.func.id
499502
else:
500503
# NOTE: SSZ definition derives from earlier phase...
501504
# e.g. `phase0.SignedBeaconBlock`

pysetup/spec_builders/gloas.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class GloasSpecBuilder(BaseSpecBuilder):
88
@classmethod
99
def imports(cls, preset_name: str):
1010
return f"""
11+
from eth2spec.utils.ssz.ssz_typing import ProgressiveBitlist, ProgressiveContainer, ProgressiveList
12+
1113
from eth2spec.fulu import {preset_name} as fulu
1214
"""
1315

pysetup/typing.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ class SpecObject(NamedTuple):
1818
functions: dict[str, str]
1919
protocols: dict[str, ProtocolDefinition]
2020
custom_types: dict[str, str]
21-
preset_dep_custom_types: dict[str, str] # the types that depend on presets
2221
constant_vars: dict[str, VariableDefinition]
2322
preset_dep_constant_vars: dict[str, VariableDefinition]
2423
preset_vars: dict[str, VariableDefinition]

specs/electra/beacon-chain.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<!-- mdformat-toc start --slug=github --no-anchors --maxlevel=6 --minlevel=2 -->
44

55
- [Introduction](#introduction)
6+
- [Custom types](#custom-types)
67
- [Constants](#constants)
78
- [Misc](#misc)
89
- [Withdrawal prefixes](#withdrawal-prefixes)
@@ -123,6 +124,16 @@ Electra is a consensus-layer upgrade containing a number of features. Including:
123124
*Note*: This specification is built upon [Deneb](../deneb/beacon-chain.md) and
124125
is under active development.
125126

127+
## Custom types
128+
129+
| Name | SSZ equivalent | Description |
130+
| ----------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
131+
| `AggregationBits` | `Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]` | Combined participation info across all participating subcommittees |
132+
| `AttestingIndices` | `List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]` | List of attesting validator indices |
133+
| `DepositRequests` | `List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]` | List of deposit requests pertaining to an execution payload |
134+
| `WithdrawalRequests` | `List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD]` | List of withdrawal requests pertaining to an execution payload |
135+
| `ConsolidationRequests` | `List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD]` | List of withdrawal requests pertaining to an execution payload |
136+
126137
## Constants
127138

128139
The following values are (non-configurable) constants used throughout the
@@ -293,11 +304,11 @@ class ConsolidationRequest(Container):
293304
```python
294305
class ExecutionRequests(Container):
295306
# [New in Electra:EIP6110]
296-
deposits: List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]
307+
deposits: DepositRequests
297308
# [New in Electra:EIP7002:EIP7251]
298-
withdrawals: List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD]
309+
withdrawals: WithdrawalRequests
299310
# [New in Electra:EIP7251]
300-
consolidations: List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD]
311+
consolidations: ConsolidationRequests
301312
```
302313

303314
#### `SingleAttestation`
@@ -351,7 +362,7 @@ class BeaconBlockBody(Container):
351362
```python
352363
class Attestation(Container):
353364
# [Modified in Electra:EIP7549]
354-
aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]
365+
aggregation_bits: AggregationBits
355366
data: AttestationData
356367
signature: BLSSignature
357368
# [New in Electra:EIP7549]
@@ -363,7 +374,7 @@ class Attestation(Container):
363374
```python
364375
class IndexedAttestation(Container):
365376
# [Modified in Electra:EIP7549]
366-
attesting_indices: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]
377+
attesting_indices: AttestingIndices
367378
data: AttestationData
368379
signature: BLSSignature
369380
```
@@ -1320,7 +1331,7 @@ def get_execution_requests_list(execution_requests: ExecutionRequests) -> Sequen
13201331
return [
13211332
request_type + ssz_serialize(request_data)
13221333
for request_type, request_data in requests
1323-
if len(request_data) != 0
1334+
if request_data
13241335
]
13251336
```
13261337

0 commit comments

Comments
 (0)