diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4b3802e0a4..e8c2e23b9c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -60,6 +60,7 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Add the `eest clean` command that helps delete generated files and directories from the repository ([#980](https://github.com/ethereum/execution-spec-tests/pull/980)). - ✨ Add framework changes for EIP-7742, required for Prague devnet-5 ([#931](https://github.com/ethereum/execution-spec-tests/pull/931)). - ✨ Add the `eest make env` command that generates a default env file (`env.yaml`)([#996](https://github.com/ethereum/execution-spec-tests/pull/996)). +- ✨ Generate Transaction Test type ([#933](https://github.com/ethereum/execution-spec-tests/pull/933)). ### 🔧 EVM Tools diff --git a/docs/consuming_tests/index.md b/docs/consuming_tests/index.md index 95d6e5cc25..071c8c8d30 100644 --- a/docs/consuming_tests/index.md +++ b/docs/consuming_tests/index.md @@ -7,6 +7,7 @@ | [State Tests](./state_test.md) | directly via a `statetest`-like command
(e.g., [go-ethereum/cmd/evm/staterunner.go](https://github.com/ethereum/go-ethereum/blob/509a64ffb9405942396276ae111d06f9bded9221/cmd/evm/staterunner.go#L35)) | `./fixtures/state_tests/` | | [Blockchain Tests](./blockchain_test.md) | directly via a `blocktest`-like command
(e.g., [go-ethereum/cmd/evm/blockrunner.go](https://github.com/ethereum/go-ethereum/blob/509a64ffb9405942396276ae111d06f9bded9221/cmd/evm/blockrunner.go#L39)) | `./fixtures/blockchain_tests/` | | [Blockchain Engine Tests](./blockchain_test_engine.md) | in the [Hive `pyspec` simulator](https://github.com/ethereum/hive/tree/master/simulators/ethereum/pyspec#readme) via the Engine API and other RPC endpoints | `./fixtures/blockchain_tests_engine/` | +| [Transaction Tests](./transaction_test.md) | directly via a `t9`-like command
(e.g., [go-ethereum's `evm t9`](https://github.com/ethereum/go-ethereum/tree/67a3b087951a3f3a8e341ae32b6ec18f3553e5cc/cmd/evm#transaction-tool)) | `./fixtures/transaction_tests/` | Here's a top-level comparison of the different methods of consuming tests: diff --git a/docs/consuming_tests/transaction_test.md b/docs/consuming_tests/transaction_test.md new file mode 100644 index 0000000000..6c60cc35b5 --- /dev/null +++ b/docs/consuming_tests/transaction_test.md @@ -0,0 +1,67 @@ +# Transaction Tests + +The Transaction Test fixture format tests are included in the fixtures subdirectory `transaction_tests`. + +These are produced by the `TransactionTest` test spec. + +## Description + +The transaction test fixture format is used to test client's transaction RLP parsing without executing the transaction on the EVM. + +It does so by defining a transaction binary RLP representation, and whether the transaction should be accepted or rejected by the client in each fork. + +A single JSON fixture file is composed of a JSON object where each key-value pair is a different [`Fixture`](#fixture) test object, with the key string representing the test name. + +The JSON file path plus the test name are used as the unique test identifier. + +The transaction test fixture format could contain multiple test vectors per test object, each represented by an element in the mapping of lists of the `result` field. + +However tests generated by the `execution-spec-tests` repository do **not** use this feature, as every single test object contains only a single test vector. + +## Consumption + +For each [`Fixture`](#fixture) test object in the JSON fixture file, perform the following steps: + +1. Obtain the [`txbytes`](#-txbytes-bytes) serialized bytes of the transaction to be parsed. +2. For each [`Fork`](./common_types.md#fork) key of [`result`](#-result-mappingforkfixtureresult) in the test: + + 1. Assume the fork schedule according to the current [`Fork`](./common_types.md#fork) key. + 2. Using the [`txbytes`](#-txbytes-bytes), attempt to decode the transaction. + 3. If the transaction could not be decoded: + - If the [`hash`](#-hash-hash-none) field is present, fail the test. + - Compare the exception thrown with the expected exception contained in the [`exception`](#-exception-transactionexception) field, and fail the test if they do not match. + - Proceed to the next fork. + 4. If the transaction could be decoded: + - Compare the calculated hash with the expected hash contained in the [`hash`](#-hash-hash-none) field, and fail the test if they do not match. + - Compare the calculated intrinsic gas with the expected intrinsic gas contained in the [`intrinsicGas`](#-intrinsicgas-zeropaddedhexnumber) field, and fail the test if they do not match. + - Compare the calculated sender with the expected sender contained in the [`sender`](#-sender-address) field, and fail the test if they do not match. + +## Structures + +### `Fixture` + +#### - `txbytes`: [`Bytes`](./common_types.md#bytes) + +Serialized bytes of the transaction under test. + +#### - `result`: [`Mapping`](./common_types.md#mapping)`[`[`Fork`](./common_types.md#fork)`,`[`FixtureResult`](#fixtureresult) `]` + +Mapping of results for verification per fork, where each key-value represents a single possible outcome of the transaction parsed in the given fork. + +### `FixtureResult` + +#### - `hash`: [`Hash`](./common_types.md#hash) `| None` + +Calculated hash of the transaction (Field is missing if the transaction is expected to fail). + +#### - `intrinsicGas`: [`ZeroPaddedHexNumber`](./common_types.md#zeropaddedhexnumber) + +Total intrinsic gas cost of the transaction (Field is missing if the transaction is expected to fail). + +#### - `sender`: [`Address`](./common_types.md#address) + +Sender address of the transaction (Field is missing if the transaction is expected to fail). + +#### - `exception`: [`TransactionException`](./exceptions.md#transactionexception) + +Exception that is expected to be thrown by the transaction parsing (Field is missing if the transaction is expected to succeed). diff --git a/docs/navigation.md b/docs/navigation.md index 7d8d057859..de930334f8 100644 --- a/docs/navigation.md +++ b/docs/navigation.md @@ -28,6 +28,7 @@ * [Blockchain Tests](consuming_tests/blockchain_test.md) * [Blockchain Engine Tests](consuming_tests/blockchain_test_engine.md) * [EOF Tests](consuming_tests/eof_test.md) + * [Transaction Tests](consuming_tests/transaction_test.md) * [Common Types](consuming_tests/common_types.md) * [Exceptions](consuming_tests/exceptions.md) * [Executing Tests](executing_tests/index.md) diff --git a/docs/writing_tests/types_of_tests.md b/docs/writing_tests/types_of_tests.md index d0421fbf6d..97c3914f53 100644 --- a/docs/writing_tests/types_of_tests.md +++ b/docs/writing_tests/types_of_tests.md @@ -1,9 +1,10 @@ # Types of tests -There are currently two types of tests that can be produced by a test spec: +There are currently three types of tests that can be produced by a test spec: 1. State Tests 2. Blockchain Tests +3. Transaction Tests ## State Tests @@ -51,6 +52,21 @@ def test_blob_type_tx_pre_fork( """ ``` +## Transaction Tests + +### Purpose + +Test correct transaction rejection/acceptance of a serialized transaction (currently RLP only). + +### Use cases + +- Verify that a badly formatted transaction is correctly rejected by the client. +- Verify that a transaction with an invalid value in one of its fields is correctly rejected by the client. + +!!! info + + Using the `execute` command, transaction tests can be sent to clients in a live network using the `eth_sendRawTransaction` endpoint. + ## Deciding on a test type ### Prefer `state_test` for single transactions diff --git a/src/cli/check_fixtures.py b/src/cli/check_fixtures.py index ef7e39f5dc..0751b85a86 100644 --- a/src/cli/check_fixtures.py +++ b/src/cli/check_fixtures.py @@ -4,6 +4,7 @@ """ from pathlib import Path +from typing import Generator import click from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn, TimeElapsedColumn @@ -59,10 +60,10 @@ def check_json(json_file_path: Path): @click.option( "--input", "-i", - "input_dir", - type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True), + "input_str", + type=click.Path(exists=True, file_okay=True, dir_okay=True, readable=True), required=True, - help="The input directory containing json fixture files", + help="The input json file or directory containing json fixture files", ) @click.option( "--quiet", @@ -83,17 +84,25 @@ def check_json(json_file_path: Path): expose_value=True, help="Stop and raise any exceptions encountered while checking fixtures.", ) -def check_fixtures(input_dir: str, quiet_mode: bool, stop_on_error: bool): +def check_fixtures(input_str: str, quiet_mode: bool, stop_on_error: bool): """ Perform some checks on the fixtures contained in the specified directory. """ - input_path = Path(input_dir) + input_path = Path(input_str) success = True file_count = 0 filename_display_width = 25 - if not quiet_mode: + if input_path.is_file(): + file_count = 1 + elif not quiet_mode: file_count = count_json_files_exclude_index(input_path) + def get_input_files() -> Generator[Path, None, None]: + if input_path.is_file(): + yield input_path + else: + yield from input_path.rglob("*.json") + with Progress( TextColumn( f"[bold cyan]{{task.fields[filename]:<{filename_display_width}}}[/]", justify="left" @@ -106,7 +115,7 @@ def check_fixtures(input_dir: str, quiet_mode: bool, stop_on_error: bool): ) as progress: task_id = progress.add_task("Checking fixtures", total=file_count, filename="...") - for json_file_path in input_path.rglob("*.json"): + for json_file_path in get_input_files(): if json_file_path.name == "index.json": continue diff --git a/src/ethereum_test_fixtures/__init__.py b/src/ethereum_test_fixtures/__init__.py index 455eac8a0b..8d1dd4ad8a 100644 --- a/src/ethereum_test_fixtures/__init__.py +++ b/src/ethereum_test_fixtures/__init__.py @@ -11,6 +11,7 @@ from .collector import FixtureCollector, TestInfo from .eof import Fixture as EOFFixture from .state import Fixture as StateFixture +from .transaction import Fixture as TransactionFixture from .verify import FixtureVerifier FIXTURE_FORMATS: Dict[str, FixtureFormat] = { @@ -20,6 +21,7 @@ BlockchainEngineFixture, EOFFixture, StateFixture, + TransactionFixture, ] } __all__ = [ @@ -34,4 +36,5 @@ "FixtureVerifier", "StateFixture", "TestInfo", + "TransactionFixture", ] diff --git a/src/ethereum_test_fixtures/base.py b/src/ethereum_test_fixtures/base.py index 2344698ff1..a584be8d2f 100644 --- a/src/ethereum_test_fixtures/base.py +++ b/src/ethereum_test_fixtures/base.py @@ -71,6 +71,7 @@ def fill_info( self.info["filling-transition-tool"] = t8n_version self.info["description"] = test_case_description self.info["url"] = fixture_source_url + self.info["fixture_format"] = self.fixture_format_name if ref_spec is not None: ref_spec.write_info(self.info) diff --git a/src/ethereum_test_fixtures/file.py b/src/ethereum_test_fixtures/file.py index 7115334948..bbb4b6bf11 100644 --- a/src/ethereum_test_fixtures/file.py +++ b/src/ethereum_test_fixtures/file.py @@ -4,7 +4,9 @@ import json from pathlib import Path -from typing import Any, Dict, Optional +from typing import Annotated, Any, Dict, Optional + +from pydantic import Discriminator, Tag from ethereum_test_base_types import EthereumTestRootModel @@ -13,8 +15,11 @@ from .blockchain import Fixture as BlockchainFixture from .eof import Fixture as EOFFixture from .state import Fixture as StateFixture +from .transaction import Fixture as TransactionFixture -FixtureModel = BlockchainFixture | BlockchainEngineFixture | StateFixture | EOFFixture +FixtureModel = ( + BlockchainFixture | BlockchainEngineFixture | StateFixture | EOFFixture | TransactionFixture +) class BaseFixturesRootModel(EthereumTestRootModel): @@ -101,6 +106,7 @@ def from_json_data( BlockchainFixture: BlockchainFixtures, BlockchainEngineFixture: BlockchainEngineFixtures, StateFixture: StateFixtures, + TransactionFixture: TransactionFixtures, EOFFixture: EOFFixtures, } @@ -114,12 +120,34 @@ def from_json_data( return model_class(root=json_data) +def fixture_format_discriminator(v: Any) -> str | None: + """ + A discriminator function that returns the model type as a string. + """ + if v is None: + return None + if isinstance(v, dict): + info_dict = v["_info"] + elif hasattr(v, "info"): + info_dict = v.info + return info_dict.get("fixture_format") + + class Fixtures(BaseFixturesRootModel): """ A model that can contain any fixture type. """ - root: Dict[str, BlockchainFixture | BlockchainEngineFixture | StateFixture] + root: Dict[ + str, + Annotated[ + Annotated[BlockchainFixture, Tag(BlockchainFixture.fixture_format_name)] + | Annotated[BlockchainEngineFixture, Tag(BlockchainEngineFixture.fixture_format_name)] + | Annotated[StateFixture, Tag(StateFixture.fixture_format_name)] + | Annotated[TransactionFixture, Tag(TransactionFixture.fixture_format_name)], + Discriminator(fixture_format_discriminator), + ], + ] class BlockchainFixtures(BaseFixturesRootModel): @@ -152,6 +180,16 @@ class StateFixtures(BaseFixturesRootModel): root: Dict[str, StateFixture] +class TransactionFixtures(BaseFixturesRootModel): + """ + Defines a top-level model containing multiple transaction test fixtures in a + dictionary of (fixture-name, fixture) pairs. This is the format used in JSON + fixture files for transaction tests. + """ + + root: Dict[str, TransactionFixture] + + class EOFFixtures(BaseFixturesRootModel): """ Defines a top-level model containing multiple state test fixtures in a diff --git a/src/ethereum_test_fixtures/transaction.py b/src/ethereum_test_fixtures/transaction.py new file mode 100644 index 0000000000..a4a0d35353 --- /dev/null +++ b/src/ethereum_test_fixtures/transaction.py @@ -0,0 +1,44 @@ +""" +TransactionTest types +""" + +from typing import ClassVar, Mapping + +from pydantic import Field + +from ethereum_test_base_types import Address, Bytes, Hash, ZeroPaddedHexNumber +from ethereum_test_exceptions import TransactionExceptionInstanceOrList +from ethereum_test_types.types import CamelModel + +from .base import BaseFixture + + +class FixtureResult(CamelModel): + """ + The per-network (fork) result structure. + """ + + hash: Hash | None = None + intrinsic_gas: ZeroPaddedHexNumber + sender: Address | None = None + exception: TransactionExceptionInstanceOrList | None = None + + +class Fixture(BaseFixture): + """ + Fixture for a single TransactionTest. + """ + + fixture_format_name: ClassVar[str] = "transaction_test" + description: ClassVar[str] = "Tests that generate a transaction test fixture." + + result: Mapping[str, FixtureResult] + transaction: Bytes = Field(..., alias="txbytes") + + def get_fork(self) -> str | None: + """ + Returns the fork of the fixture as a string. + """ + forks = list(self.result.keys()) + assert len(forks) == 1, "Expected transaction test fixture with single fork" + return forks[0] diff --git a/src/ethereum_test_forks/base_fork.py b/src/ethereum_test_forks/base_fork.py index 4ba6808479..7e31a527b2 100644 --- a/src/ethereum_test_forks/base_fork.py +++ b/src/ethereum_test_forks/base_fork.py @@ -3,7 +3,7 @@ """ from abc import ABC, ABCMeta, abstractmethod -from typing import Any, ClassVar, List, Mapping, Optional, Protocol, Tuple, Type +from typing import Any, ClassVar, List, Mapping, Optional, Protocol, Sized, Tuple, Type from semver import Version @@ -62,7 +62,7 @@ def __call__( calldata: BytesConvertible = b"", contract_creation: bool = False, access_list: List[AccessList] | None = None, - authorization_count: int | None = None, + authorization_list_or_count: Sized | int | None = None, ) -> int: """ Returns the intrinsic gas cost of a transaction given its properties. diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 1ea4fa50cb..c7385c7555 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -6,7 +6,7 @@ from hashlib import sha256 from os.path import realpath from pathlib import Path -from typing import List, Mapping, Optional, Tuple +from typing import List, Mapping, Optional, Sized, Tuple from semver import Version @@ -210,10 +210,12 @@ def fn( calldata: BytesConvertible = b"", contract_creation: bool = False, access_list: List[AccessList] | None = None, - authorization_count: int | None = None, + authorization_list_or_count: Sized | int | None = None, ) -> int: assert access_list is None, f"Access list is not supported in {cls.name()}" - assert authorization_count is None, f"Authorizations are not supported in {cls.name()}" + assert ( + authorization_list_or_count is None + ), f"Authorizations are not supported in {cls.name()}" intrinsic_cost: int = gas_costs.G_TRANSACTION if contract_creation: @@ -630,13 +632,13 @@ def fn( calldata: BytesConvertible = b"", contract_creation: bool = False, access_list: List[AccessList] | None = None, - authorization_count: int | None = None, + authorization_list_or_count: Sized | int | None = None, ) -> int: intrinsic_cost: int = super_fn( calldata=calldata, contract_creation=contract_creation, access_list=access_list, - authorization_count=authorization_count, + authorization_list_or_count=authorization_list_or_count, ) if contract_creation: intrinsic_cost += gas_costs.G_TRANSACTION_CREATE @@ -815,12 +817,12 @@ def fn( calldata: BytesConvertible = b"", contract_creation: bool = False, access_list: List[AccessList] | None = None, - authorization_count: int | None = None, + authorization_list_or_count: Sized | int | None = None, ) -> int: intrinsic_cost: int = super_fn( calldata=calldata, contract_creation=contract_creation, - authorization_count=authorization_count, + authorization_list_or_count=authorization_list_or_count, ) if access_list is not None: for access in access_list: @@ -1161,15 +1163,17 @@ def fn( calldata: BytesConvertible = b"", contract_creation: bool = False, access_list: List[AccessList] | None = None, - authorization_count: int | None = None, + authorization_list_or_count: Sized | int | None = None, ) -> int: intrinsic_cost: int = super_fn( calldata=calldata, contract_creation=contract_creation, access_list=access_list, ) - if authorization_count is not None: - intrinsic_cost += authorization_count * gas_costs.G_AUTHORIZATION + if authorization_list_or_count is not None: + if isinstance(authorization_list_or_count, Sized): + authorization_list_or_count = len(authorization_list_or_count) + intrinsic_cost += authorization_list_or_count * gas_costs.G_AUTHORIZATION return intrinsic_cost return fn diff --git a/src/ethereum_test_specs/__init__.py b/src/ethereum_test_specs/__init__.py index 0e7d9b2b6b..fc9d8ef69a 100644 --- a/src/ethereum_test_specs/__init__.py +++ b/src/ethereum_test_specs/__init__.py @@ -22,6 +22,7 @@ EOFTestSpec, ) from .state import StateTest, StateTestFiller, StateTestOnly, StateTestSpec +from .transaction import TransactionTest, TransactionTestFiller, TransactionTestSpec SPEC_TYPES: List[Type[BaseTest]] = [ BlockchainTest, @@ -30,6 +31,7 @@ StateTestOnly, EOFTest, EOFStateTest, + TransactionTest, ] @@ -53,4 +55,7 @@ "StateTestOnly", "StateTestSpec", "TestSpec", + "TransactionTest", + "TransactionTestFiller", + "TransactionTestSpec", ) diff --git a/src/ethereum_test_specs/tests/fixtures/blockchain_london_invalid_filled.json b/src/ethereum_test_specs/tests/fixtures/blockchain_london_invalid_filled.json index 3ee643a0bc..2e1148de8e 100644 --- a/src/ethereum_test_specs/tests/fixtures/blockchain_london_invalid_filled.json +++ b/src/ethereum_test_specs/tests/fixtures/blockchain_london_invalid_filled.json @@ -1,7 +1,8 @@ { "000/my_blockchain_test/London": { "_info": { - "hash": "0x4de3f84e3cb1e678141d81ce96ce75edb53f1824a708e26098b610c3c1030e66" + "hash": "0x4de3f84e3cb1e678141d81ce96ce75edb53f1824a708e26098b610c3c1030e66", + "fixture_format": "blockchain_test" }, "network": "London", "genesisRLP": "0xf90200f901fba00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a089a5be1d3306f6f05b42678ef13ac3dbc37bef9a2a80862c21eb22eee29194c2a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200008088016345785d8a0000808000a000000000000000000000000000000000000000000000000000000000000000008800000000000000008203e8c0c0", diff --git a/src/ethereum_test_specs/tests/fixtures/blockchain_london_valid_filled.json b/src/ethereum_test_specs/tests/fixtures/blockchain_london_valid_filled.json index d8e6edb3b7..e711471643 100644 --- a/src/ethereum_test_specs/tests/fixtures/blockchain_london_valid_filled.json +++ b/src/ethereum_test_specs/tests/fixtures/blockchain_london_valid_filled.json @@ -1,7 +1,8 @@ { "000/my_blockchain_test/London": { "_info": { - "hash": "0x91032fb245f4488b204198312cbf16429c121435705ac3f9c6eb3943ec0bc36d" + "hash": "0x91032fb245f4488b204198312cbf16429c121435705ac3f9c6eb3943ec0bc36d", + "fixture_format": "blockchain_test" }, "network": "London", "genesisRLP": "0xf90200f901fba00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a089a5be1d3306f6f05b42678ef13ac3dbc37bef9a2a80862c21eb22eee29194c2a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200008088016345785d8a0000808000a000000000000000000000000000000000000000000000000000000000000000008800000000000000008203e8c0c0", diff --git a/src/ethereum_test_specs/tests/fixtures/blockchain_shanghai_invalid_filled_engine.json b/src/ethereum_test_specs/tests/fixtures/blockchain_shanghai_invalid_filled_engine.json index 33f7a73f7b..c113c079e9 100644 --- a/src/ethereum_test_specs/tests/fixtures/blockchain_shanghai_invalid_filled_engine.json +++ b/src/ethereum_test_specs/tests/fixtures/blockchain_shanghai_invalid_filled_engine.json @@ -1,7 +1,8 @@ { "000/my_blockchain_test/Shanghai": { "_info": { - "hash": "0x107426e7483fe00d8db263f7522d523a6efbed5c93fd98006e65593ce496a1c4" + "hash": "0x107426e7483fe00d8db263f7522d523a6efbed5c93fd98006e65593ce496a1c4", + "fixture_format": "blockchain_test_engine" }, "lastblockhash": "0xfc75f11c05ec814a890141bef919bb7c20dd29245e37e9bcea66008dfde98526", "network": "Shanghai", diff --git a/src/ethereum_test_specs/tests/fixtures/blockchain_shanghai_valid_filled_engine.json b/src/ethereum_test_specs/tests/fixtures/blockchain_shanghai_valid_filled_engine.json index 3270566ec0..77a95f81ac 100644 --- a/src/ethereum_test_specs/tests/fixtures/blockchain_shanghai_valid_filled_engine.json +++ b/src/ethereum_test_specs/tests/fixtures/blockchain_shanghai_valid_filled_engine.json @@ -1,7 +1,8 @@ { "000/my_blockchain_test/Shanghai": { "_info": { - "hash": "0x9a25679729dab0fa4d90f56a7458ca2c4b7428853e9ef1e1aea6dae203926368" + "hash": "0x9a25679729dab0fa4d90f56a7458ca2c4b7428853e9ef1e1aea6dae203926368", + "fixture_format": "blockchain_test_engine" }, "lastblockhash": "0xfc75f11c05ec814a890141bef919bb7c20dd29245e37e9bcea66008dfde98526", "network": "Shanghai", diff --git a/src/ethereum_test_specs/tests/fixtures/chainid_istanbul_blockchain_test.json b/src/ethereum_test_specs/tests/fixtures/chainid_istanbul_blockchain_test.json index 040110f9c5..3ab1e609be 100644 --- a/src/ethereum_test_specs/tests/fixtures/chainid_istanbul_blockchain_test.json +++ b/src/ethereum_test_specs/tests/fixtures/chainid_istanbul_blockchain_test.json @@ -1,7 +1,8 @@ { "000/my_chain_id_test/Istanbul": { "_info": { - "hash": "0x3ca9936ff21270dd7ac781b5fafd98e4264bc9fcff4ab3cc8dff0677ccf7fc25" + "hash": "0x3ca9936ff21270dd7ac781b5fafd98e4264bc9fcff4ab3cc8dff0677ccf7fc25", + "fixture_format": "blockchain_test" }, "network": "Istanbul", "genesisRLP": "0xf901faf901f5a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0aff9f63320a482f8c4e4f15f659e6a7ac382138fbbb6919243b0cba4c5988a5aa056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000808502540be400808000a00000000000000000000000000000000000000000000000000000000000000000880000000000000000c0c0", diff --git a/src/ethereum_test_specs/tests/fixtures/chainid_london_blockchain_test.json b/src/ethereum_test_specs/tests/fixtures/chainid_london_blockchain_test.json index 0681eb1fe5..a9c4ccf293 100644 --- a/src/ethereum_test_specs/tests/fixtures/chainid_london_blockchain_test.json +++ b/src/ethereum_test_specs/tests/fixtures/chainid_london_blockchain_test.json @@ -1,7 +1,8 @@ { "000/my_chain_id_test/London": { "_info": { - "hash": "0x9c09a561959f81ff5e5b081b9081bd626739fa029e9d411ea89797673366eb80" + "hash": "0x9c09a561959f81ff5e5b081b9081bd626739fa029e9d411ea89797673366eb80", + "fixture_format": "blockchain_test" }, "network": "London", "genesisRLP": "0xf901fbf901f6a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0aff9f63320a482f8c4e4f15f659e6a7ac382138fbbb6919243b0cba4c5988a5aa056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000808502540be400808000a0000000000000000000000000000000000000000000000000000000000000000088000000000000000007c0c0", diff --git a/src/ethereum_test_specs/tests/fixtures/chainid_paris_blockchain_test_engine.json b/src/ethereum_test_specs/tests/fixtures/chainid_paris_blockchain_test_engine.json index 792fb4798f..82c45e2b79 100644 --- a/src/ethereum_test_specs/tests/fixtures/chainid_paris_blockchain_test_engine.json +++ b/src/ethereum_test_specs/tests/fixtures/chainid_paris_blockchain_test_engine.json @@ -1,7 +1,8 @@ { "000/my_chain_id_test/Paris": { "_info": { - "hash": "0x1c6d2ca8e03c5074e8afadbad869c90b61d2f9752c5ef2d7908900aa000a3878" + "hash": "0x1c6d2ca8e03c5074e8afadbad869c90b61d2f9752c5ef2d7908900aa000a3878", + "fixture_format": "blockchain_test_engine" }, "network": "Paris", "lastblockhash": "0xe92eedff2a0489bd861f528e248994b6791b0f5b845d90b34c68bc8cbc51c369", diff --git a/src/ethereum_test_specs/tests/fixtures/chainid_paris_state_test.json b/src/ethereum_test_specs/tests/fixtures/chainid_paris_state_test.json index bfe2cd2b2c..ea64221c1b 100644 --- a/src/ethereum_test_specs/tests/fixtures/chainid_paris_state_test.json +++ b/src/ethereum_test_specs/tests/fixtures/chainid_paris_state_test.json @@ -1,7 +1,8 @@ { "000/my_chain_id_test/Paris": { "_info": { - "hash": "0x9533310242d3fe15fca5ea1d31f97121f7db0f54843c9f2160c01f2468b10535" + "hash": "0x9533310242d3fe15fca5ea1d31f97121f7db0f54843c9f2160c01f2468b10535", + "fixture_format": "state_test" }, "env": { "currentCoinbase": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", diff --git a/src/ethereum_test_specs/tests/fixtures/chainid_shanghai_blockchain_test_engine.json b/src/ethereum_test_specs/tests/fixtures/chainid_shanghai_blockchain_test_engine.json index 8f94dc8e94..24785254ee 100644 --- a/src/ethereum_test_specs/tests/fixtures/chainid_shanghai_blockchain_test_engine.json +++ b/src/ethereum_test_specs/tests/fixtures/chainid_shanghai_blockchain_test_engine.json @@ -1,7 +1,8 @@ { "000/my_chain_id_test/Shanghai": { "_info": { - "hash": "0xfeded8f82a93725388c2436c76ea328cf2008dab43000de76306b3ed95de63b7" + "hash": "0xfeded8f82a93725388c2436c76ea328cf2008dab43000de76306b3ed95de63b7", + "fixture_format": "blockchain_test_engine" }, "lastblockhash": "0x9c10141361e180632f7973f4f3a0aed2baa5ebb776bae84caafdcc07a24933e8", "network": "Shanghai", diff --git a/src/ethereum_test_specs/tests/fixtures/chainid_shanghai_state_test.json b/src/ethereum_test_specs/tests/fixtures/chainid_shanghai_state_test.json index 97c25aa49b..4571094018 100644 --- a/src/ethereum_test_specs/tests/fixtures/chainid_shanghai_state_test.json +++ b/src/ethereum_test_specs/tests/fixtures/chainid_shanghai_state_test.json @@ -1,7 +1,8 @@ { "000/my_chain_id_test/Shanghai": { "_info": { - "hash": "0xac10a919bea8bb3bc6b74cb291d92ce12549216dd78dde2d2bd6d94fd48897aa" + "hash": "0xac10a919bea8bb3bc6b74cb291d92ce12549216dd78dde2d2bd6d94fd48897aa", + "fixture_format": "state_test" }, "env": { "currentCoinbase": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", diff --git a/src/ethereum_test_specs/tests/fixtures/tx_simple_type_0_shanghai.json b/src/ethereum_test_specs/tests/fixtures/tx_simple_type_0_shanghai.json new file mode 100644 index 0000000000..4dfc66a72c --- /dev/null +++ b/src/ethereum_test_specs/tests/fixtures/tx_simple_type_0_shanghai.json @@ -0,0 +1,16 @@ +{ + "fixture": { + "_info" : { + "hash" : "0x3c588e18802a3c0c875da0e7bc8f9066369572c30d11e2568a516650216a33e7", + "fixture_format": "transaction_test" + }, + "result" : { + "Shanghai" : { + "hash" : "0x1997251035c9109e5cad5b146251579be13bbd91a17bf628b7cbb5f25dad73e2", + "intrinsicGas" : "0x5208", + "sender" : "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" + } + }, + "txbytes" : "0xf85f800a8252089400000000000000000000000000000000000000aa808026a0cc61d852649c34cc0b71803115f38036ace257d2914f087bf885e6806a664fbda02020cb35f5d7731ab540d62614503a7f2344301a86342f67daf011c1341551ff" + } +} \ No newline at end of file diff --git a/src/ethereum_test_specs/tests/helpers.py b/src/ethereum_test_specs/tests/helpers.py new file mode 100644 index 0000000000..58dde9ef50 --- /dev/null +++ b/src/ethereum_test_specs/tests/helpers.py @@ -0,0 +1,12 @@ +""" +Helper methods used in the spec tests. +""" + + +def remove_info_metadata(fixture_json): # noqa: D103 + for t in fixture_json: + if "_info" in fixture_json[t]: + info_keys = list(fixture_json[t]["_info"].keys()) + for key in info_keys: + if key != "hash": # remove keys that are not 'hash' + del fixture_json[t]["_info"][key] diff --git a/src/ethereum_test_specs/tests/test_fixtures.py b/src/ethereum_test_specs/tests/test_fixtures.py index 551386c2f2..9c92e51496 100644 --- a/src/ethereum_test_specs/tests/test_fixtures.py +++ b/src/ethereum_test_specs/tests/test_fixtures.py @@ -26,15 +26,7 @@ from ..blockchain import Block, BlockchainTest, Header from ..state import StateTest - - -def remove_info_metadata(fixture_json): # noqa: D103 - for t in fixture_json: - if "_info" in fixture_json[t]: - info_keys = list(fixture_json[t]["_info"].keys()) - for key in info_keys: - if key != "hash": # remove keys that are not 'hash' - del fixture_json[t]["_info"][key] +from .helpers import remove_info_metadata @pytest.fixture() @@ -187,6 +179,7 @@ def test_fill_state_test( ) ) as f: expected = json.load(f) + remove_info_metadata(expected) remove_info_metadata(fixture) assert fixture == expected @@ -532,6 +525,7 @@ def test_fill_blockchain_valid_txs( # noqa: D102 ) ) as f: expected = json.load(f) + remove_info_metadata(expected) remove_info_metadata(fixture) assert fixture_name in fixture @@ -906,6 +900,7 @@ def test_fill_blockchain_invalid_txs(fork: Fork, check_hive: bool, expected_json ) ) as f: expected = json.load(f) + remove_info_metadata(expected) remove_info_metadata(fixture) assert fixture_name in fixture diff --git a/src/ethereum_test_specs/tests/test_transaction.py b/src/ethereum_test_specs/tests/test_transaction.py new file mode 100644 index 0000000000..7d34dec5fb --- /dev/null +++ b/src/ethereum_test_specs/tests/test_transaction.py @@ -0,0 +1,53 @@ +""" +Test suite for the transaction spec test generation. +""" +import json +import os + +import pytest + +from ethereum_test_fixtures import TransactionFixture +from ethereum_test_forks import Fork, Shanghai +from ethereum_test_types import Transaction + +from ..transaction import TransactionTest +from .helpers import remove_info_metadata + + +@pytest.mark.parametrize( + "name, tx, fork", + [ + pytest.param("simple_type_0", Transaction(), Shanghai), + ], +) +def test_transaction_test_filling(name: str, tx: Transaction, fork: Fork): + """ + Test the transaction test filling. + """ + generated_fixture = TransactionTest(tx=tx.with_signature_and_sender()).generate( + request=None, # type: ignore + t8n=None, # type: ignore + fork=fork, + fixture_format=TransactionFixture, + ) + assert generated_fixture.__class__ == TransactionFixture + fixture_json_dict = generated_fixture.json_dict_with_info() + fixture = { + "fixture": fixture_json_dict, + } + + expected_json_file = f"tx_{name}_{fork.name().lower()}.json" + with open( + os.path.join( + "src", + "ethereum_test_specs", + "tests", + "fixtures", + expected_json_file, + ) + ) as f: + expected = json.load(f) + remove_info_metadata(expected) + + remove_info_metadata(fixture) + assert fixture == expected diff --git a/src/ethereum_test_specs/transaction.py b/src/ethereum_test_specs/transaction.py new file mode 100644 index 0000000000..3184318e39 --- /dev/null +++ b/src/ethereum_test_specs/transaction.py @@ -0,0 +1,106 @@ +""" +Ethereum transaction test spec definition and filler. +""" + +from typing import Callable, ClassVar, Generator, List, Optional, Type + +import pytest + +from ethereum_clis import TransitionTool +from ethereum_test_execution import BaseExecute, ExecuteFormat, TransactionPost +from ethereum_test_fixtures import BaseFixture, FixtureFormat, TransactionFixture +from ethereum_test_fixtures.transaction import Fixture, FixtureResult +from ethereum_test_forks import Fork +from ethereum_test_types import Alloc, Transaction + +from .base import BaseTest + + +class TransactionTest(BaseTest): + """ + Filler type that tests the transaction over the period of a single block. + """ + + tx: Transaction + pre: Alloc | None = None + + supported_fixture_formats: ClassVar[List[FixtureFormat]] = [ + TransactionFixture, + ] + supported_execute_formats: ClassVar[List[ExecuteFormat]] = [ + TransactionPost, + ] + + def make_transaction_test_fixture( + self, + fork: Fork, + eips: Optional[List[int]] = None, + ) -> Fixture: + """ + Create a fixture from the transaction test definition. + """ + if self.tx.error is not None: + result = FixtureResult( + exception=self.tx.error, + hash=None, + intrinsic_gas=0, + sender=None, + ) + else: + intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas = intrinsic_gas_cost_calculator( + calldata=self.tx.data, + contract_creation=self.tx.to is None, + access_list=self.tx.access_list, + authorization_list_or_count=self.tx.authorization_list, + ) + result = FixtureResult( + exception=None, + hash=self.tx.hash, + intrinsic_gas=intrinsic_gas, + sender=self.tx.sender, + ) + + return Fixture( + result={ + fork.blockchain_test_network_name(): result, + }, + transaction=self.tx.with_signature_and_sender().rlp, + ) + + def generate( + self, + request: pytest.FixtureRequest, + t8n: TransitionTool, + fork: Fork, + fixture_format: FixtureFormat, + eips: Optional[List[int]] = None, + ) -> BaseFixture: + """ + Generate the TransactionTest fixture. + """ + if fixture_format == TransactionFixture: + return self.make_transaction_test_fixture(fork, eips) + + raise Exception(f"Unknown fixture format: {fixture_format}") + + def execute( + self, + *, + fork: Fork, + execute_format: ExecuteFormat, + eips: Optional[List[int]] = None, + ) -> BaseExecute: + """ + Execute the transaction test by sending it to the live network. + """ + if execute_format == TransactionPost: + return TransactionPost( + transactions=[self.tx], + post={}, + ) + raise Exception(f"Unsupported execute format: {execute_format}") + + +TransactionTestSpec = Callable[[str], Generator[TransactionTest, None, None]] +TransactionTestFiller = Type[TransactionTest] diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 60854d4d8d..39b5866bdc 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -35,6 +35,8 @@ EOFTestFiller, StateTest, StateTestFiller, + TransactionTest, + TransactionTestFiller, ) from ethereum_test_specs.blockchain import Block, Header from ethereum_test_types import ( @@ -140,6 +142,8 @@ "TestPrivateKey2", "Transaction", "TransactionException", + "TransactionTest", + "TransactionTestFiller", "Withdrawal", "WithdrawalRequest", "Yul", diff --git a/src/ethereum_test_types/tests/test_types.py b/src/ethereum_test_types/tests/test_types.py index 08ab049bd4..770ee6c42c 100644 --- a/src/ethereum_test_types/tests/test_types.py +++ b/src/ethereum_test_types/tests/test_types.py @@ -729,6 +729,20 @@ def test_transaction_post_init_invalid_arg_combinations( # noqa: D103 ], id="ty-2-adds-max_priority_fee_per_gas", ), + pytest.param( + {"to": Address(1)}, + [ + ("to", Address(1)), + ], + id="non-zero-to", + ), + pytest.param( + {"to": Address(0)}, + [ + ("to", Address(0)), + ], + id="zero-to", + ), ], ) def test_transaction_post_init_defaults(tx_args, expected_attributes_and_values): diff --git a/src/ethereum_test_types/types.py b/src/ethereum_test_types/types.py index cb3bcfba7d..06ae84a0c7 100644 --- a/src/ethereum_test_types/types.py +++ b/src/ethereum_test_types/types.py @@ -497,7 +497,7 @@ class AuthorizationTupleGeneric(CamelModel, Generic[NumberBoundTypeVar]): chain_id: NumberBoundTypeVar = Field(0) # type: ignore address: Address - nonce: NumberBoundTypeVar = Field(0) # type: ignore + nonce: List[NumberBoundTypeVar] | NumberBoundTypeVar = Field(0) # type: ignore v: NumberBoundTypeVar = Field(0) # type: ignore r: NumberBoundTypeVar = Field(0) # type: ignore @@ -509,6 +509,16 @@ def to_list(self) -> List[Any]: """ Returns the authorization tuple as a list of serializable elements. """ + if isinstance(self.nonce, list): + # Nonce list for testing purposes only + return [ + Uint(self.chain_id), + self.address, + [Uint(nonce) for nonce in self.nonce], + Uint(self.v), + Uint(self.r), + Uint(self.s), + ] return [ Uint(self.chain_id), self.address, @@ -523,6 +533,18 @@ def signing_bytes(self) -> Bytes: """ Returns the data to be signed. """ + if isinstance(self.nonce, list): + # Nonce list for testing purposes only + return Bytes( + int.to_bytes(self.magic, length=1, byteorder="big") + + eth_rlp.encode( + [ + Uint(self.chain_id), + self.address, + [Uint(nonce) for nonce in self.nonce], + ] + ) + ) return Bytes( int.to_bytes(self.magic, length=1, byteorder="big") + eth_rlp.encode( @@ -676,7 +698,12 @@ def validate_to_as_empty_string(cls, data: Any) -> Any: """ If the `to` field is an empty string, set the model value to None. """ - if isinstance(data, dict) and "to" in data and data["to"] == "": + if ( + isinstance(data, dict) + and "to" in data + and isinstance(data["to"], str) + and data["to"] == "" + ): data["to"] = None return data diff --git a/tests/prague/eip7702_set_code_tx/spec.py b/tests/prague/eip7702_set_code_tx/spec.py index dcd95642a9..5cbd68fd50 100644 --- a/tests/prague/eip7702_set_code_tx/spec.py +++ b/tests/prague/eip7702_set_code_tx/spec.py @@ -34,6 +34,9 @@ class Spec: DELEGATION_DESIGNATION_READING = Bytes("ef01") RESET_DELEGATION_ADDRESS = Address(0) + MAX_CHAIN_ID = 2**64 - 1 + MAX_NONCE = 2**64 - 1 + @staticmethod def delegation_designation(address: Address) -> Bytes: """ diff --git a/tests/prague/eip7702_set_code_tx/test_gas.py b/tests/prague/eip7702_set_code_tx/test_gas.py index 7f04be43ab..6891cb7a22 100644 --- a/tests/prague/eip7702_set_code_tx/test_gas.py +++ b/tests/prague/eip7702_set_code_tx/test_gas.py @@ -696,7 +696,7 @@ def test_gas_cost( intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( calldata=data, access_list=access_list, - authorization_count=len(authorization_list), + authorization_list_or_count=authorization_list, ) discounted_authorizations = 0 @@ -949,7 +949,7 @@ def test_intrinsic_gas_cost( intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( calldata=data, access_list=access_list, - authorization_count=len(authorization_list), + authorization_list_or_count=authorization_list, ) tx_gas = intrinsic_gas diff --git a/tests/prague/eip7702_set_code_tx/test_invalid_tx.py b/tests/prague/eip7702_set_code_tx/test_invalid_tx.py new file mode 100644 index 0000000000..b2c9c61707 --- /dev/null +++ b/tests/prague/eip7702_set_code_tx/test_invalid_tx.py @@ -0,0 +1,196 @@ +""" +abstract: Tests invalid set-code transactions from [EIP-7702: Set EOA account code for one transaction](https://eips.ethereum.org/EIPS/eip-7702) + Tests invalid set-code transactions from [EIP-7702: Set EOA account code for one transaction](https://eips.ethereum.org/EIPS/eip-7702). +""" # noqa: E501 + +from typing import List + +import pytest + +from ethereum_test_tools import ( + Address, + Alloc, + AuthorizationTuple, + Transaction, + TransactionException, + TransactionTestFiller, +) + +from .spec import Spec, ref_spec_7702 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7702.git_path +REFERENCE_SPEC_VERSION = ref_spec_7702.version + +pytestmark = pytest.mark.valid_from("Prague") + +auth_account_start_balance = 0 + + +def test_empty_authorization_list( + transaction_test: TransactionTestFiller, + pre: Alloc, +): + """ + Test sending a transaction with an empty authorization list. + """ + tx = Transaction( + gas_limit=100_000, + to=0, + value=0, + authorization_list=[], + error=TransactionException.TYPE_4_EMPTY_AUTHORIZATION_LIST, + sender=pre.fund_eoa(), + ) + transaction_test( + pre=pre, + tx=tx, + ) + + +@pytest.mark.parametrize( + "v,r,s", + [ + pytest.param(2**8, 1, 1, id="v=2**8"), + pytest.param(1, 2**256, 1, id="r=2**256"), + pytest.param(1, 1, 2**256, id="s=2**256"), + pytest.param(2**8, 2**256, 2**256, id="v=2**8,r=s=2**256"), + ], +) +@pytest.mark.parametrize( + "delegate_address", + [ + pytest.param(Spec.RESET_DELEGATION_ADDRESS, id="reset_delegation_address"), + pytest.param(Address(1), id="non_zero_address"), + ], +) +def test_invalid_auth_signature( + transaction_test: TransactionTestFiller, + pre: Alloc, + v: int, + r: int, + s: int, + delegate_address: Address, +): + """ + Test sending a transaction where one of the signature elements is out of range. + """ + tx = Transaction( + gas_limit=100_000, + to=0, + value=0, + authorization_list=[ + AuthorizationTuple( + address=delegate_address, + nonce=0, + chain_id=1, + v=v, + r=r, + s=s, + ), + ], + error=[ + TransactionException.TYPE_4_INVALID_AUTHORITY_SIGNATURE, + TransactionException.TYPE_4_INVALID_AUTHORITY_SIGNATURE_S_TOO_HIGH, + ], + sender=pre.fund_eoa(), + ) + + transaction_test( + pre=pre, + tx=tx, + ) + + +@pytest.mark.parametrize( + "chain_id", + [ + pytest.param(Spec.MAX_CHAIN_ID + 1, id="chain_id=2**64"), + pytest.param(2**256, id="chain_id=2**256"), + ], +) +@pytest.mark.parametrize( + "delegate_address", + [ + pytest.param(Spec.RESET_DELEGATION_ADDRESS, id="reset_delegation_address"), + pytest.param(Address(1), id="non_zero_address"), + ], +) +def test_invalid_tx_invalid_chain_id( + transaction_test: TransactionTestFiller, + pre: Alloc, + chain_id: int, + delegate_address: Address, +): + """ + Test sending a transaction where the chain id field of an authorization overflows the + maximum value. + """ + authorization = AuthorizationTuple( + address=delegate_address, + nonce=0, + chain_id=chain_id, + signer=pre.fund_eoa(auth_account_start_balance), + ) + + tx = Transaction( + gas_limit=100_000, + to=0, + value=0, + authorization_list=[authorization], + error=TransactionException.TYPE_4_INVALID_AUTHORIZATION_FORMAT, + sender=pre.fund_eoa(), + ) + + transaction_test( + pre=pre, + tx=tx, + ) + + +@pytest.mark.parametrize( + "nonce", + [ + pytest.param(Spec.MAX_NONCE + 1, id="nonce=2**64"), + pytest.param(2**256, id="nonce=2**256"), + pytest.param([], id="nonce=empty-list"), + pytest.param([0], id="nonce=non-empty-list"), + ], +) +@pytest.mark.parametrize( + "delegate_address", + [ + pytest.param(Spec.RESET_DELEGATION_ADDRESS, id="reset_delegation_address"), + pytest.param(Address(1), id="non_zero_address"), + ], +) +def test_invalid_tx_invalid_nonce( + transaction_test: TransactionTestFiller, + pre: Alloc, + nonce: int | List[int], + delegate_address: Address, +): + """ + Test sending a transaction where the nonce field of an authorization overflows the maximum + value. + """ + auth_signer = pre.fund_eoa() + + tx = Transaction( + gas_limit=100_000, + to=0, + value=0, + authorization_list=[ + AuthorizationTuple( + address=delegate_address, + nonce=nonce, + signer=auth_signer, + ), + ], + error=TransactionException.TYPE_4_INVALID_AUTHORIZATION_FORMAT, + sender=pre.fund_eoa(), + ) + + transaction_test( + pre=pre, + tx=tx, + ) diff --git a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py index 042b6a67aa..ee819b3d71 100644 --- a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py +++ b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py @@ -3,10 +3,8 @@ Tests use of set-code transactions from [EIP-7702: Set EOA account code for one transaction](https://eips.ethereum.org/EIPS/eip-7702). """ # noqa: E501 -from enum import Enum from hashlib import sha256 from itertools import count -from typing import List import pytest @@ -2040,93 +2038,6 @@ def test_set_code_all_invalid_authorization_tuples( ) -class InvalidityReason(Enum): - """ - Reasons for invalidity of a set-code transaction. - """ - - NONCE = "nonce" - MULTIPLE_NONCE = "multiple_nonce" - CHAIN_ID = "chain_id" - EMPTY_AUTHORIZATION_LIST = "empty_authorization_list" - INVALID_SIGNATURE_S_VALUE = "invalid_signature_s_value" # TODO: Implement - - -@pytest.mark.parametrize( - "invalidity_reason,transaction_exception", - [ - pytest.param( - InvalidityReason.NONCE, - None, # Transaction is valid and accepted, but no authorization tuple is processed - ), - pytest.param( - InvalidityReason.MULTIPLE_NONCE, - None, - marks=pytest.mark.xfail(reason="test issue"), - ), - pytest.param( - InvalidityReason.CHAIN_ID, - None, # Transaction is valid and accepted, but no authorization tuple is processed - ), - pytest.param( - InvalidityReason.EMPTY_AUTHORIZATION_LIST, - TransactionException.TYPE_4_EMPTY_AUTHORIZATION_LIST, - ), - ], -) -def test_set_code_invalid_authorization_tuple( - state_test: StateTestFiller, - pre: Alloc, - invalidity_reason: InvalidityReason, - transaction_exception: TransactionException | None, -): - """ - Test attempting to set the code of an account with invalid authorization tuple. - """ - auth_signer = pre.fund_eoa(auth_account_start_balance) - - success_slot = 1 - - set_code = Op.SSTORE(success_slot, 1) + Op.STOP - set_code_to_address = pre.deploy_contract(set_code) - - authorization_list: List[AuthorizationTuple] = [] - - if invalidity_reason != InvalidityReason.EMPTY_AUTHORIZATION_LIST: - authorization_list = [ - AuthorizationTuple( - address=set_code_to_address, - nonce=( - 1 - if invalidity_reason == InvalidityReason.NONCE - else [0, 1] - if invalidity_reason == InvalidityReason.MULTIPLE_NONCE - else 0 - ), - chain_id=2 if invalidity_reason == InvalidityReason.CHAIN_ID else 0, - signer=auth_signer, - ) - ] - - tx = Transaction( - gas_limit=10_000_000, - to=auth_signer, - value=0, - authorization_list=authorization_list, - error=transaction_exception, - sender=pre.fund_eoa(), - ) - - state_test( - env=Environment(), - pre=pre, - tx=tx, - post={ - auth_signer: Account.NONEXISTENT, - }, - ) - - def test_set_code_using_chain_specific_id( state_test: StateTestFiller, pre: Alloc, @@ -2237,64 +2148,6 @@ def test_set_code_using_valid_synthetic_signatures( ) -# TODO: invalid RLP in the rest of the authority tuple fields -@pytest.mark.parametrize( - "v,r,s", - [ - pytest.param(2**8, 1, 1, id="v=2**8"), - pytest.param(1, 2**256, 1, id="r=2**256"), - pytest.param(1, 1, 2**256, id="s=2**256"), - pytest.param(2**8, 2**256, 2**256, id="v=2**8,r=s=2**256"), - ], -) -def test_invalid_tx_invalid_auth_signature( - state_test: StateTestFiller, - pre: Alloc, - v: int, - r: int, - s: int, -): - """ - Test sending a transaction to set the code of an account using synthetic signatures. - """ - success_slot = 1 - - callee_code = Op.SSTORE(success_slot, 1) + Op.STOP - callee_address = pre.deploy_contract(callee_code) - - authorization_tuple = AuthorizationTuple( - address=0, - nonce=0, - chain_id=1, - v=v, - r=r, - s=s, - ) - - tx = Transaction( - gas_limit=100_000, - to=callee_address, - value=0, - authorization_list=[authorization_tuple], - error=[ - TransactionException.TYPE_4_INVALID_AUTHORITY_SIGNATURE, - TransactionException.TYPE_4_INVALID_AUTHORITY_SIGNATURE_S_TOO_HIGH, - ], - sender=pre.fund_eoa(), - ) - - state_test( - env=Environment(), - pre=pre, - tx=tx, - post={ - callee_address: Account( - storage={success_slot: 0}, - ), - }, - ) - - @pytest.mark.parametrize( "v,r,s", [ @@ -2421,23 +2274,19 @@ def test_signature_s_out_of_range( @pytest.mark.parametrize( - "chain_id,transaction_exception", + "chain_id", [ - pytest.param( - 2**64, TransactionException.TYPE_4_INVALID_AUTHORIZATION_FORMAT, id="chain_id=2**64" - ), - pytest.param(2**64 - 1, None, id="chain_id=2**64-1"), + pytest.param(Spec.MAX_CHAIN_ID, id="chain_id=2**64-1"), + pytest.param(2, id="chain_id=2"), ], ) -def test_tx_validity_chain_id( +def test_valid_tx_invalid_chain_id( state_test: StateTestFiller, pre: Alloc, chain_id: int, - transaction_exception: TransactionException | None, ): """ - Test sending a transaction where the chain id field of an authorization overflows the - maximum value, or almost overflows the maximum value. + Test sending a transaction where the chain id field does not match the current chain's id. """ auth_signer = pre.fund_eoa(auth_account_start_balance) @@ -2466,7 +2315,7 @@ def test_tx_validity_chain_id( to=entry_address, value=0, authorization_list=[authorization], - error=transaction_exception, + error=None, sender=pre.fund_eoa(), ) @@ -2478,7 +2327,7 @@ def test_tx_validity_chain_id( auth_signer: Account.NONEXISTENT, entry_address: Account( storage={ - success_slot: 1 if transaction_exception is None else 0, + success_slot: 1, return_slot: 0, }, ), @@ -2487,52 +2336,59 @@ def test_tx_validity_chain_id( @pytest.mark.parametrize( - "nonce,transaction_exception", + "account_nonce,authorization_nonce", [ pytest.param( - 2**64, TransactionException.TYPE_4_INVALID_AUTHORIZATION_FORMAT, id="nonce=2**64" - ), - pytest.param( - 2**64 - 1, - None, + Spec.MAX_NONCE, + Spec.MAX_NONCE, id="nonce=2**64-1", marks=pytest.mark.execute(pytest.mark.skip(reason="Impossible account nonce")), ), pytest.param( - 2**64 - 2, - None, + Spec.MAX_NONCE - 1, + Spec.MAX_NONCE - 1, id="nonce=2**64-2", marks=pytest.mark.execute(pytest.mark.skip(reason="Impossible account nonce")), ), + pytest.param( + 0, + 1, + id="nonce=1,account_nonce=0", + ), + pytest.param( + 1, + 0, + id="nonce=0,account_nonce=1", + ), ], ) -def test_tx_validity_nonce( +def test_nonce_validity( state_test: StateTestFiller, pre: Alloc, - nonce: int, - transaction_exception: TransactionException | None, + account_nonce: int, + authorization_nonce: int, ): """ - Test sending a transaction where the nonce field of an authorization overflows the maximum - value, or almost overflows the maximum value. + Test sending a transaction where the nonce field of an authorization almost overflows the + maximum value. Also test calling the account of the authorization signer in order to verify that the account is not warm. """ - auth_signer = pre.fund_eoa( - auth_account_start_balance, nonce=nonce if nonce < 2**64 else None - ) + auth_signer = pre.fund_eoa(auth_account_start_balance, nonce=account_nonce) success_slot = 1 return_slot = 2 - valid_authorization = nonce < 2**64 - 1 + valid_authorization = ( + authorization_nonce < 2**64 - 1 and account_nonce == authorization_nonce + ) set_code = Op.RETURN(0, 1) set_code_to_address = pre.deploy_contract(set_code) authorization = AuthorizationTuple( address=set_code_to_address, - nonce=nonce, + nonce=authorization_nonce, signer=auth_signer, ) @@ -2548,7 +2404,6 @@ def test_tx_validity_nonce( to=entry_address, value=0, authorization_list=[authorization], - error=transaction_exception, sender=pre.fund_eoa(), ) @@ -2558,16 +2413,16 @@ def test_tx_validity_nonce( tx=tx, post={ auth_signer: Account( - nonce=(nonce + 1) if (nonce < (2**64 - 1)) else nonce, + nonce=(account_nonce + 1) if valid_authorization else account_nonce, code=Spec.delegation_designation(set_code_to_address) if valid_authorization else b"", ) - if nonce < 2**64 + if authorization_nonce < 2**64 and account_nonce > 0 else Account.NONEXISTENT, entry_address: Account( storage={ - success_slot: 1 if transaction_exception is None else 0, + success_slot: 1, return_slot: 1 if valid_authorization else 0, }, ),