diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f2b093aa9e..0563366e59 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -24,6 +24,7 @@ As part of the pydantic conversion, the fixtures have the following (possibly br - `expectException` fields now print the exceptions in sorted order. - State test field `transaction` now uses the proper zero-padded hex number format for fields `maxPriorityFeePerGas`, `maxFeePerGas`, and `maxFeePerBlobGas` +- Fixtures' hashes (in the `_info` field) are now calculated by removing the "_info" field entirely instead of it being set to an empty dict. ## 🔜 [v2.1.1](https://github.com/ethereum/execution-spec-tests/releases/tag/v2.1.1) - 2024-03-09 diff --git a/src/ethereum_test_tools/spec/base/base_test.py b/src/ethereum_test_tools/spec/base/base_test.py index 04e8fb3e8c..17263b1466 100644 --- a/src/ethereum_test_tools/spec/base/base_test.py +++ b/src/ethereum_test_tools/spec/base/base_test.py @@ -5,7 +5,7 @@ import hashlib import json from abc import abstractmethod -from functools import reduce +from functools import cached_property, reduce from itertools import count from os import path from pathlib import Path @@ -18,7 +18,6 @@ from ...common import Environment, Transaction, Withdrawal from ...common.conversions import to_hex -from ...common.json import to_json from ...common.types import CamelModel, Result from ...reference_spec.reference_spec import ReferenceSpec @@ -67,15 +66,36 @@ def verify_result(result: Result, env: Environment): class BaseFixture(CamelModel): - """ - Represents a base Ethereum test fixture of any type. - """ + """Represents a base Ethereum test fixture of any type.""" info: Dict[str, str] = Field(default_factory=dict, alias="_info") + format: ClassVar[FixtureFormats] = FixtureFormats.UNSET_TEST_FORMAT - _json: Optional[Dict[str, Any]] = None + @cached_property + def json_dict(self) -> Dict[str, Any]: + """ + Returns the JSON representation of the fixture. + """ + return self.model_dump(mode="json", by_alias=True, exclude_none=True, exclude={"_info"}) - format: ClassVar[FixtureFormats] = FixtureFormats.UNSET_TEST_FORMAT + @cached_property + def hash(self) -> str: + """ + Returns the hash of the fixture. + """ + json_str = json.dumps(self.json_dict, sort_keys=True, separators=(",", ":")) + h = hashlib.sha256(json_str.encode("utf-8")).hexdigest() + return f"0x{h}" + + def json_dict_with_info(self, hash_only: bool = False) -> Dict[str, Any]: + """ + Returns the JSON representation of the fixture with the info field. + """ + dict_with_info = self.json_dict.copy() + dict_with_info["_info"] = {"hash": self.hash} + if not hash_only: + dict_with_info["_info"].update(self.info) + return dict_with_info def fill_info( self, @@ -91,39 +111,6 @@ def fill_info( if ref_spec is not None: ref_spec.write_info(self.info) - def model_post_init(self, __context): - """ - Post init hook to convert to JSON after instantiation. - """ - super().model_post_init(__context) - previous_hash = None - if "hash" in self.info: # e.g., we're loading an existing fixture from file - previous_hash = self.info["hash"] - self._json = to_json(self) - self.add_hash() - if previous_hash and previous_hash != self.info["hash"]: - raise HashMismatchException(previous_hash, self.info["hash"]) - - def to_json(self) -> Dict[str, Any]: - """ - Convert to JSON. - """ - assert self._json is not None, "Fixture not initialized" - self._json["_info"] = self.info - return self._json - - def add_hash(self) -> None: - """ - Calculate the hash of the fixture and add it to the fixture and fixture's - json. - """ - assert self._json is not None, "Fixture not initialized" - self._json["_info"] = {} - json_str = json.dumps(self._json, sort_keys=True, separators=(",", ":")) - h = hashlib.sha256(json_str.encode("utf-8")).hexdigest() - self.info["hash"] = f"0x{h}" - self._json["_info"] = self.info - @classmethod def collect_into_file(cls, fd: TextIO, fixtures: Dict[str, "BaseFixture"]): """ @@ -132,7 +119,7 @@ def collect_into_file(cls, fd: TextIO, fixtures: Dict[str, "BaseFixture"]): json_fixtures: Dict[str, Dict[str, Any]] = {} for name, fixture in fixtures.items(): assert isinstance(fixture, cls), f"Invalid fixture type: {type(fixture)}" - json_fixtures[name] = fixture.to_json() + json_fixtures[name] = fixture.json_dict_with_info() json.dump(json_fixtures, fd, indent=4) diff --git a/src/ethereum_test_tools/tests/test_filling/test_fixtures.py b/src/ethereum_test_tools/tests/test_filling/test_fixtures.py index f3cbb5cde7..c80e3ce7e6 100644 --- a/src/ethereum_test_tools/tests/test_filling/test_fixtures.py +++ b/src/ethereum_test_tools/tests/test_filling/test_fixtures.py @@ -14,7 +14,7 @@ from ... import Header from ...code import Yul -from ...common import Account, Environment, Hash, TestAddress, Transaction, to_json +from ...common import Account, Environment, Hash, TestAddress, Transaction from ...exceptions import TransactionException from ...spec import BlockchainTest, StateTest from ...spec.blockchain.types import Block @@ -156,7 +156,7 @@ def test_fill_state_test( ) assert generated_fixture.format == fixture_format fixture = { - f"000/my_chain_id_test/{fork}": to_json(generated_fixture), + f"000/my_chain_id_test/{fork}": generated_fixture.json_dict_with_info(hash_only=True), } expected_json_file = f"chainid_{fork.name().lower()}_{fixture_format.value}.json" @@ -499,7 +499,9 @@ def test_fill_blockchain_valid_txs( # noqa: D102 assert isinstance(blockchain_test_fixture, BlockchainFixtureCommon) fixture = { - f"000/my_blockchain_test/{fork.name()}": to_json(blockchain_test_fixture), + f"000/my_blockchain_test/{fork.name()}": blockchain_test_fixture.json_dict_with_info( + hash_only=True + ), } with open( @@ -872,7 +874,9 @@ def test_fill_blockchain_invalid_txs( assert generated_fixture.format == fixture_format assert isinstance(generated_fixture, BlockchainFixtureCommon) fixture = { - f"000/my_blockchain_test/{fork.name()}": to_json(generated_fixture), + f"000/my_blockchain_test/{fork.name()}": generated_fixture.json_dict_with_info( + hash_only=True + ), } with open(