diff --git a/Makefile b/Makefile index e4969ff2f..511100e75 100644 --- a/Makefile +++ b/Makefile @@ -96,11 +96,11 @@ install_test: test: pyspec . venv/bin/activate; cd $(PY_SPEC_DIR); \ - python3 -m pytest -n 4 --disable-bls --cov=eth2spec.phase0.spec --cov=eth2spec.altair.spec --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec + python3 -m pytest -n 4 --disable-bls --cov=eth2spec.phase0.mainnet --cov=eth2spec.altair.mainnet --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec find_test: pyspec . venv/bin/activate; cd $(PY_SPEC_DIR); \ - python3 -m pytest -k=$(K) --disable-bls --cov=eth2spec.phase0.spec --cov=eth2spec.altair.spec --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec + python3 -m pytest -k=$(K) --disable-bls --cov=eth2spec.phase0.mainnet --cov=eth2spec.altair.mainnet --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec citest: pyspec mkdir -p tests/core/pyspec/test-reports/eth2spec; . venv/bin/activate; cd $(PY_SPEC_DIR); \ diff --git a/setup.py b/setup.py index fec0cc7f3..b98dcf26c 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ class ProtocolDefinition(NamedTuple): class VariableDefinition(NamedTuple): - type_name: str + type_name: Optional[str] value: str comment: Optional[str] # e.g. "noqa: E501" @@ -145,7 +145,7 @@ def _parse_value(name: str, typed_value: str) -> VariableDefinition: typed_value = typed_value.strip() if '(' not in typed_value: - return VariableDefinition(type_name='int', value=typed_value, comment=comment) + return VariableDefinition(type_name=None, value=typed_value, comment=comment) i = typed_value.index('(') type_name = typed_value[:i] @@ -259,7 +259,7 @@ class SpecBuilder(ABC): @classmethod @abstractmethod - def imports(cls) -> str: + def imports(cls, preset_name: str) -> str: """ Import objects from other libraries. """ @@ -307,7 +307,8 @@ class SpecBuilder(ABC): @classmethod @abstractmethod - def build_spec(cls, source_files: List[Path], preset_files: Sequence[Path], config_file: Path) -> str: + def build_spec(cls, preset_name: str, + source_files: List[Path], preset_files: Sequence[Path], config_file: Path) -> str: raise NotImplementedError() @@ -318,7 +319,7 @@ class Phase0SpecBuilder(SpecBuilder): fork: str = PHASE0 @classmethod - def imports(cls) -> str: + def imports(cls, preset_name: str) -> str: return '''from lru import LRU from dataclasses import ( dataclass, @@ -429,8 +430,9 @@ get_attesting_indices = cache_this( return '' @classmethod - def build_spec(cls, source_files: Sequence[Path], preset_files: Sequence[Path], config_file: Path) -> str: - return _build_spec(cls.fork, source_files, preset_files, config_file) + def build_spec(cls, preset_name: str, + source_files: Sequence[Path], preset_files: Sequence[Path], config_file: Path) -> str: + return _build_spec(preset_name, cls.fork, source_files, preset_files, config_file) # @@ -440,21 +442,17 @@ class AltairSpecBuilder(Phase0SpecBuilder): fork: str = ALTAIR @classmethod - def imports(cls) -> str: - return super().imports() + '\n' + ''' + def imports(cls, preset_name: str) -> str: + return super().imports(preset_name) + '\n' + f''' from typing import NewType, Union -from importlib import reload -from eth2spec.phase0 import spec as phase0 +from eth2spec.phase0 import {preset_name} as phase0 from eth2spec.utils.ssz.ssz_typing import Path ''' @classmethod def preparations(cls): return super().preparations() + '\n' + ''' -# Whenever this spec version is loaded, make sure we have the latest phase0 -reload(phase0) - SSZVariableName = str GeneralizedIndex = NewType('GeneralizedIndex', int) ''' @@ -492,19 +490,16 @@ class MergeSpecBuilder(Phase0SpecBuilder): fork: str = MERGE @classmethod - def imports(cls): - return super().imports() + ''' + def imports(cls, preset_name: str): + return super().imports(preset_name) + f''' from typing import Protocol -from eth2spec.phase0 import spec as phase0 +from eth2spec.phase0 import {preset_name} as phase0 from eth2spec.utils.ssz.ssz_typing import Bytes20, ByteList, ByteVector, uint256 -from importlib import reload ''' @classmethod def preparations(cls): - return super().preparations() + '\n' + ''' -reload(phase0) -''' + return super().preparations() @classmethod def sundry_functions(cls) -> str: @@ -513,7 +508,7 @@ ExecutionState = Any def get_pow_block(hash: Bytes32) -> PowBlock: - return PowBlock(block_hash=hash, is_valid=True, is_processed=True, total_difficulty=TRANSITION_TOTAL_DIFFICULTY) + return PowBlock(block_hash=hash, is_valid=True, is_processed=True, total_difficulty=config.TRANSITION_TOTAL_DIFFICULTY) def get_execution_state(execution_state_root: Bytes32) -> ExecutionState: @@ -556,7 +551,10 @@ spec_builders = { } -def objects_to_spec(spec_object: SpecObject, builder: SpecBuilder, ordered_class_objects: Dict[str, str]) -> str: +def objects_to_spec(preset_name: str, + spec_object: SpecObject, + builder: SpecBuilder, + ordered_class_objects: Dict[str, str]) -> str: """ Given all the objects that constitute a spec, combine them into a single pyfile. """ @@ -596,19 +594,28 @@ def objects_to_spec(spec_object: SpecObject, builder: SpecBuilder, ordered_class functions_spec = functions_spec.replace(name, 'config.' + name) def format_config_var(name: str, vardef: VariableDefinition) -> str: - out = f'{name}={vardef.type_name}({vardef.value}),' + if vardef.type_name is None: + out = f'{name}={vardef.value}' + else: + out = f'{name}={vardef.type_name}({vardef.value}),' if vardef.comment is not None: out += f' # {vardef.comment}' return out config_spec = '@dataclass\nclass Configuration(object):\n' - config_spec += '\n'.join(f' {k}: {v.type_name}' for k, v in spec_object.config_vars.items()) + config_spec += ' PRESET_BASE: str\n' + config_spec += '\n'.join(f' {k}: {v.type_name if v.type_name is not None else "int"}' + for k, v in spec_object.config_vars.items()) config_spec += '\n\n\nconfig = Configuration(\n' + config_spec += f' PRESET_BASE="{preset_name}",\n' config_spec += '\n'.join(' ' + format_config_var(k, v) for k, v in spec_object.config_vars.items()) config_spec += '\n)\n' def format_constant(name: str, vardef: VariableDefinition) -> str: - out = f'{name} = {vardef.type_name}({vardef.value})' + if vardef.type_name is None: + out = f'{name} = {vardef.value}' + else: + out = f'{name} = {vardef.type_name}({vardef.value})' if vardef.comment is not None: out += f' # {vardef.comment}' return out @@ -620,7 +627,7 @@ def objects_to_spec(spec_object: SpecObject, builder: SpecBuilder, ordered_class ssz_dep_constants_verification = '\n'.join(map(lambda x: 'assert %s == %s' % (x, spec_object.ssz_dep_constants[x]), builder.hardcoded_ssz_dep_constants())) custom_type_dep_constants = '\n'.join(map(lambda x: '%s = %s' % (x, builder.hardcoded_custom_type_dep_constants()[x]), builder.hardcoded_custom_type_dep_constants())) spec = ( - builder.imports() + builder.imports(preset_name) + builder.preparations() + '\n\n' + f"fork = \'{builder.fork}\'\n" # The constants that some SSZ containers require. Need to be defined before `new_type_definitions` @@ -785,7 +792,8 @@ def load_config(config_path: Path) -> Dict[str, str]: return parse_config_vars(config_data) -def _build_spec(fork: str, source_files: Sequence[Path], preset_files: Sequence[Path], config_file: Path) -> str: +def _build_spec(preset_name: str, fork: str, + source_files: Sequence[Path], preset_files: Sequence[Path], config_file: Path) -> str: preset = load_preset(preset_files) config = load_config(config_file) all_specs = [get_spec(spec, preset, config) for spec in source_files] @@ -797,7 +805,7 @@ def _build_spec(fork: str, source_files: Sequence[Path], preset_files: Sequence[ class_objects = {**spec_object.ssz_objects, **spec_object.dataclasses} dependency_order_class_objects(class_objects, spec_object.custom_types) - return objects_to_spec(spec_object, spec_builders[fork], class_objects) + return objects_to_spec(preset_name, spec_object, spec_builders[fork], class_objects) class BuildTarget(NamedTuple): @@ -903,7 +911,8 @@ class PySpecCommand(Command): dir_util.mkpath(self.out_dir) for (name, preset_paths, config_path) in self.parsed_build_targets: - spec_str = spec_builders[self.spec_fork].build_spec(self.parsed_md_doc_paths, preset_paths, config_path) + spec_str = spec_builders[self.spec_fork].build_spec( + name, self.parsed_md_doc_paths, preset_paths, config_path) if self.dry_run: self.announce('dry run successfully prepared contents for spec.' f' out dir: "{self.out_dir}", spec fork: "{self.spec_fork}", build target: "{name}"') diff --git a/tests/core/pyspec/eth2spec/test/conftest.py b/tests/core/pyspec/eth2spec/test/conftest.py index ca7516a2e..f247cc8f0 100644 --- a/tests/core/pyspec/eth2spec/test/conftest.py +++ b/tests/core/pyspec/eth2spec/test/conftest.py @@ -28,8 +28,8 @@ def fixture(*args, **kwargs): def pytest_addoption(parser): parser.addoption( - "--config", action="store", type=str, default="minimal", - help="config: make the pyspec use the specified configuration" + "--preset", action="store", type=str, default="minimal", + help="preset: make the pyspec use the specified preset" ) parser.addoption( "--disable-bls", action="store_true", default=False, @@ -42,18 +42,10 @@ def pytest_addoption(parser): @fixture(autouse=True) -def config(request): - if not config_util.loaded_defaults: - config_util.load_defaults(Path("../../../configs")) - - config_flag_value = request.config.getoption("--config") - if config_flag_value in ('minimal', 'mainnet'): - config_util.prepare_config(config_flag_value) - else: - # absolute network config path, e.g. run tests with testnet config - config_util.prepare_config(Path(config_flag_value)) - # now that the presets are loaded, reload the specs to apply them - context.reload_specs() +def preset(request): + # TODO: apply to tests, see context.py 'with_presets' + preset_flag_value = request.config.getoption("--preset") + print("preset:", preset_flag_value) @fixture(autouse=True) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index f7a408f31..4a3c9acb5 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -1,31 +1,23 @@ import pytest - from copy import deepcopy -from eth2spec.phase0 import spec as spec_phase0 -from eth2spec.altair import spec as spec_altair -from eth2spec.merge import spec as spec_merge +from eth2spec.phase0 import mainnet as spec_phase0_mainnet, minimal as spec_phase0_minimal +from eth2spec.altair import mainnet as spec_altair_mainnet, minimal as spec_altair_minimal +from eth2spec.merge import mainnet as spec_merge_mainnet, minimal as spec_merge_minimal from eth2spec.utils import bls from .exceptions import SkippedTest from .helpers.constants import ( - PHASE0, ALTAIR, MERGE, + SpecForkName, PresetBaseName, + PHASE0, ALTAIR, MERGE, MINIMAL, MAINNET, ALL_PHASES, FORKS_BEFORE_ALTAIR, FORKS_BEFORE_MERGE, ) from .helpers.genesis import create_genesis_state from .utils import vector_test, with_meta_tags, build_transition_test from random import Random -from typing import Any, Callable, Sequence, TypedDict, Protocol +from typing import Any, Callable, Sequence, TypedDict, Protocol, Dict from lru import LRU -from eth2spec.config import config_util -from importlib import reload - - -def reload_specs(): - reload(spec_phase0) - reload(spec_altair) - reload(spec_merge) # TODO: currently phases are defined as python modules. @@ -48,6 +40,20 @@ class SpecMerge(Spec): ... +spec_targets: Dict[PresetBaseName, Dict[SpecForkName, Spec]] = { + MINIMAL: { + PHASE0: spec_phase0_minimal, + ALTAIR: spec_altair_minimal, + MERGE: spec_merge_minimal, + }, + MAINNET: { + PHASE0: spec_phase0_mainnet, + ALTAIR: spec_altair_mainnet, + MERGE: spec_merge_mainnet, + }, +} + + class SpecForks(TypedDict, total=False): PHASE0: SpecPhase0 ALTAIR: SpecAltair @@ -73,7 +79,7 @@ def with_custom_state(balances_fn: Callable[[Any], Sequence[int]], def entry(*args, spec: Spec, phases: SpecForks, **kw): # make a key for the state - key = (spec.fork, spec.CONFIG_NAME, spec.__file__, balances_fn, threshold_fn) + key = (spec.fork, spec.config.PRESET_BASE, spec.__file__, balances_fn, threshold_fn) global _custom_state_cache_dict if key not in _custom_state_cache_dict: state = _prepare_state(balances_fn, threshold_fn, spec, phases) @@ -321,6 +327,11 @@ def with_phases(phases, other_phases=None): if other_phases is not None: available_phases |= set(other_phases) + preset_name = MINIMAL + if 'preset' in kw: + preset_name = kw.pop('preset') + targets = spec_targets[preset_name] + # TODO: test state is dependent on phase0 but is immediately transitioned to later phases. # A new state-creation helper for later phases may be in place, and then tests can run without phase0 available_phases.add(PHASE0) @@ -328,20 +339,20 @@ def with_phases(phases, other_phases=None): # Populate all phases for multi-phase tests phase_dir = {} if PHASE0 in available_phases: - phase_dir[PHASE0] = spec_phase0 + phase_dir[PHASE0] = targets[PHASE0] if ALTAIR in available_phases: - phase_dir[ALTAIR] = spec_altair + phase_dir[ALTAIR] = targets[ALTAIR] if MERGE in available_phases: - phase_dir[MERGE] = spec_merge + phase_dir[MERGE] = targets[MERGE] # return is ignored whenever multiple phases are ran. # This return is for test generators to emit python generators (yielding test vector outputs) if PHASE0 in run_phases: - ret = fn(spec=spec_phase0, phases=phase_dir, *args, **kw) + ret = fn(spec=targets[PHASE0], phases=phase_dir, *args, **kw) if ALTAIR in run_phases: - ret = fn(spec=spec_altair, phases=phase_dir, *args, **kw) + ret = fn(spec=targets[ALTAIR], phases=phase_dir, *args, **kw) if MERGE in run_phases: - ret = fn(spec=spec_merge, phases=phase_dir, *args, **kw) + ret = fn(spec=targets[MERGE], phases=phase_dir, *args, **kw) # TODO: merge, sharding, custody_game and das are not executable yet. # Tests that specify these features will not run, and get ignored for these specific phases. @@ -351,12 +362,12 @@ def with_phases(phases, other_phases=None): def with_presets(preset_bases, reason=None): - available_configs = set(preset_bases) + available_presets = set(preset_bases) def decorator(fn): def wrapper(*args, spec: Spec, **kw): - if spec.PRESET_BASE not in available_configs: - message = f"doesn't support this preset base: {spec.PRESET_BASE}." + if spec.config.PRESET_BASE not in available_presets: + message = f"doesn't support this preset base: {spec.config.PRESET_BASE}." if reason is not None: message = f"{message} Reason: {reason}" dump_skipping_message(message) @@ -376,13 +387,12 @@ def with_config_overrides(config_overrides): def decorator(fn): def wrapper(*args, spec: Spec, **kw): # remember the old config - old_config = config_util.config + old_config = spec.config # apply our overrides to a copy of it, and apply it to the spec tmp_config = deepcopy(old_config) tmp_config.update(config_overrides) - config_util.config = tmp_config - reload_specs() # Note this reloads the same module instance(s) that we passed into the test + spec.config = tmp_config # Run the function out = fn(*args, spec=spec, **kw) @@ -392,8 +402,7 @@ def with_config_overrides(config_overrides): yield from out # Restore the old config and apply it - config_util.config = old_config - reload_specs() + spec.config = old_config return wrapper return decorator diff --git a/tests/core/pyspec/eth2spec/test/helpers/keys.py b/tests/core/pyspec/eth2spec/test/helpers/keys.py index d813870e0..5e36e90df 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/keys.py +++ b/tests/core/pyspec/eth2spec/test/helpers/keys.py @@ -1,6 +1,6 @@ from py_ecc.bls import G2ProofOfPossession as bls -from eth2spec.phase0 import spec -privkeys = [i + 1 for i in range(spec.SLOTS_PER_EPOCH * 256)] +# Enough keys for 256 validators per slot in worst-case epoch length +privkeys = [i + 1 for i in range(32 * 256)] pubkeys = [bls.SkToPk(privkey) for privkey in privkeys] pubkey_to_privkey = {pubkey: privkey for privkey, pubkey in zip(privkeys, pubkeys)}