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,
},
),