Skip to content

Commit 4aa6bc0

Browse files
feat(fixtures,specs,tests): Transaction tests, EIP-7702 convert invalid tx tests (ethereum#933)
* new(fixtures): TransactionFixture format * new(specs): TransactionTest format * fix(forks): rename intrinsic cost calc parameter name * fix(tests): rename intrinsic cost calc parameter name * fix(specs): use `fork` intrinsic gas calculator * fix(fixtures): Add `TransactionFixture` to file * fix(fixtures): transaction type optional fields * feat(fixtures): Add fixture format to `_info` for easier parsing * fix(cli/check_fixtures): Allow a single fixture check * fix(types): Address(0) == "" * fix(fixtures,specs): fixes * feat(types): Allow nonce list in auth tuple, for testing purposes * fix(tests): EIP-7702: Convert invalid tx tests * docs: update, changelog * fix(docs): tox * fix(tests): EIP-7702: Move invalid tx tests to its own file * new(docs): Add Transaction Test to `consuming_tests` * fix(fixtures): fix `fixture_type_discriminator` * Update docs/consuming_tests/index.md Co-authored-by: danceratopz <[email protected]> * Apply suggestions from code review (fixture_type_discriminator) Co-authored-by: danceratopz <[email protected]> * nit --------- Co-authored-by: danceratopz <[email protected]>
1 parent d65e6fb commit 4aa6bc0

35 files changed

+707
-226
lines changed

Diff for: docs/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Test fixtures for use by clients are available for each release on the [Github r
6060
- ✨ 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)).
6161
- ✨ Add framework changes for EIP-7742, required for Prague devnet-5 ([#931](https://github.com/ethereum/execution-spec-tests/pull/931)).
6262
- ✨ Add the `eest make env` command that generates a default env file (`env.yaml`)([#996](https://github.com/ethereum/execution-spec-tests/pull/996)).
63+
- ✨ Generate Transaction Test type ([#933](https://github.com/ethereum/execution-spec-tests/pull/933)).
6364

6465
### 🔧 EVM Tools
6566

Diff for: docs/consuming_tests/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
| [State Tests](./state_test.md) | directly via a `statetest`-like command<br/> (e.g., [go-ethereum/cmd/evm/staterunner.go](https://github.com/ethereum/go-ethereum/blob/509a64ffb9405942396276ae111d06f9bded9221/cmd/evm/staterunner.go#L35)) | `./fixtures/state_tests/` |
88
| [Blockchain Tests](./blockchain_test.md) | directly via a `blocktest`-like command<br/> (e.g., [go-ethereum/cmd/evm/blockrunner.go](https://github.com/ethereum/go-ethereum/blob/509a64ffb9405942396276ae111d06f9bded9221/cmd/evm/blockrunner.go#L39)) | `./fixtures/blockchain_tests/` |
99
| [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/` |
10+
| [Transaction Tests](./transaction_test.md) | directly via a `t9`-like command<br/> (e.g., [go-ethereum's `evm t9`](https://github.com/ethereum/go-ethereum/tree/67a3b087951a3f3a8e341ae32b6ec18f3553e5cc/cmd/evm#transaction-tool)) | `./fixtures/transaction_tests/` |
1011

1112
Here's a top-level comparison of the different methods of consuming tests:
1213

Diff for: docs/consuming_tests/transaction_test.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Transaction Tests <!-- markdownlint-disable MD051 (MD051=link-fragments "Link fragments should be valid") -->
2+
3+
The Transaction Test fixture format tests are included in the fixtures subdirectory `transaction_tests`.
4+
5+
These are produced by the `TransactionTest` test spec.
6+
7+
## Description
8+
9+
The transaction test fixture format is used to test client's transaction RLP parsing without executing the transaction on the EVM.
10+
11+
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.
12+
13+
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.
14+
15+
The JSON file path plus the test name are used as the unique test identifier.
16+
17+
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.
18+
19+
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.
20+
21+
## Consumption
22+
23+
For each [`Fixture`](#fixture) test object in the JSON fixture file, perform the following steps:
24+
25+
1. Obtain the [`txbytes`](#-txbytes-bytes) serialized bytes of the transaction to be parsed.
26+
2. For each [`Fork`](./common_types.md#fork) key of [`result`](#-result-mappingforkfixtureresult) in the test:
27+
28+
1. Assume the fork schedule according to the current [`Fork`](./common_types.md#fork) key.
29+
2. Using the [`txbytes`](#-txbytes-bytes), attempt to decode the transaction.
30+
3. If the transaction could not be decoded:
31+
- If the [`hash`](#-hash-hash-none) field is present, fail the test.
32+
- Compare the exception thrown with the expected exception contained in the [`exception`](#-exception-transactionexception) field, and fail the test if they do not match.
33+
- Proceed to the next fork.
34+
4. If the transaction could be decoded:
35+
- 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.
36+
- 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.
37+
- Compare the calculated sender with the expected sender contained in the [`sender`](#-sender-address) field, and fail the test if they do not match.
38+
39+
## Structures
40+
41+
### `Fixture`
42+
43+
#### - `txbytes`: [`Bytes`](./common_types.md#bytes)
44+
45+
Serialized bytes of the transaction under test.
46+
47+
#### - `result`: [`Mapping`](./common_types.md#mapping)`[`[`Fork`](./common_types.md#fork)`,`[`FixtureResult`](#fixtureresult) `]`
48+
49+
Mapping of results for verification per fork, where each key-value represents a single possible outcome of the transaction parsed in the given fork.
50+
51+
### `FixtureResult`
52+
53+
#### - `hash`: [`Hash`](./common_types.md#hash) `| None`
54+
55+
Calculated hash of the transaction (Field is missing if the transaction is expected to fail).
56+
57+
#### - `intrinsicGas`: [`ZeroPaddedHexNumber`](./common_types.md#zeropaddedhexnumber)
58+
59+
Total intrinsic gas cost of the transaction (Field is missing if the transaction is expected to fail).
60+
61+
#### - `sender`: [`Address`](./common_types.md#address)
62+
63+
Sender address of the transaction (Field is missing if the transaction is expected to fail).
64+
65+
#### - `exception`: [`TransactionException`](./exceptions.md#transactionexception)
66+
67+
Exception that is expected to be thrown by the transaction parsing (Field is missing if the transaction is expected to succeed).

Diff for: docs/navigation.md

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* [Blockchain Tests](consuming_tests/blockchain_test.md)
2929
* [Blockchain Engine Tests](consuming_tests/blockchain_test_engine.md)
3030
* [EOF Tests](consuming_tests/eof_test.md)
31+
* [Transaction Tests](consuming_tests/transaction_test.md)
3132
* [Common Types](consuming_tests/common_types.md)
3233
* [Exceptions](consuming_tests/exceptions.md)
3334
* [Executing Tests](executing_tests/index.md)

Diff for: docs/writing_tests/types_of_tests.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Types of tests
22

3-
There are currently two types of tests that can be produced by a test spec:
3+
There are currently three types of tests that can be produced by a test spec:
44

55
1. State Tests
66
2. Blockchain Tests
7+
3. Transaction Tests
78

89
## State Tests
910

@@ -51,6 +52,21 @@ def test_blob_type_tx_pre_fork(
5152
"""
5253
```
5354

55+
## Transaction Tests
56+
57+
### Purpose
58+
59+
Test correct transaction rejection/acceptance of a serialized transaction (currently RLP only).
60+
61+
### Use cases
62+
63+
- Verify that a badly formatted transaction is correctly rejected by the client.
64+
- Verify that a transaction with an invalid value in one of its fields is correctly rejected by the client.
65+
66+
!!! info
67+
68+
Using the `execute` command, transaction tests can be sent to clients in a live network using the `eth_sendRawTransaction` endpoint.
69+
5470
## Deciding on a test type
5571

5672
### Prefer `state_test` for single transactions

Diff for: src/cli/check_fixtures.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
from pathlib import Path
7+
from typing import Generator
78

89
import click
910
from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn, TimeElapsedColumn
@@ -59,10 +60,10 @@ def check_json(json_file_path: Path):
5960
@click.option(
6061
"--input",
6162
"-i",
62-
"input_dir",
63-
type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True),
63+
"input_str",
64+
type=click.Path(exists=True, file_okay=True, dir_okay=True, readable=True),
6465
required=True,
65-
help="The input directory containing json fixture files",
66+
help="The input json file or directory containing json fixture files",
6667
)
6768
@click.option(
6869
"--quiet",
@@ -83,17 +84,25 @@ def check_json(json_file_path: Path):
8384
expose_value=True,
8485
help="Stop and raise any exceptions encountered while checking fixtures.",
8586
)
86-
def check_fixtures(input_dir: str, quiet_mode: bool, stop_on_error: bool):
87+
def check_fixtures(input_str: str, quiet_mode: bool, stop_on_error: bool):
8788
"""
8889
Perform some checks on the fixtures contained in the specified directory.
8990
"""
90-
input_path = Path(input_dir)
91+
input_path = Path(input_str)
9192
success = True
9293
file_count = 0
9394
filename_display_width = 25
94-
if not quiet_mode:
95+
if input_path.is_file():
96+
file_count = 1
97+
elif not quiet_mode:
9598
file_count = count_json_files_exclude_index(input_path)
9699

100+
def get_input_files() -> Generator[Path, None, None]:
101+
if input_path.is_file():
102+
yield input_path
103+
else:
104+
yield from input_path.rglob("*.json")
105+
97106
with Progress(
98107
TextColumn(
99108
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):
106115
) as progress:
107116

108117
task_id = progress.add_task("Checking fixtures", total=file_count, filename="...")
109-
for json_file_path in input_path.rglob("*.json"):
118+
for json_file_path in get_input_files():
110119
if json_file_path.name == "index.json":
111120
continue
112121

Diff for: src/ethereum_test_fixtures/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .collector import FixtureCollector, TestInfo
1212
from .eof import Fixture as EOFFixture
1313
from .state import Fixture as StateFixture
14+
from .transaction import Fixture as TransactionFixture
1415
from .verify import FixtureVerifier
1516

1617
FIXTURE_FORMATS: Dict[str, FixtureFormat] = {
@@ -20,6 +21,7 @@
2021
BlockchainEngineFixture,
2122
EOFFixture,
2223
StateFixture,
24+
TransactionFixture,
2325
]
2426
}
2527
__all__ = [
@@ -34,4 +36,5 @@
3436
"FixtureVerifier",
3537
"StateFixture",
3638
"TestInfo",
39+
"TransactionFixture",
3740
]

Diff for: src/ethereum_test_fixtures/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def fill_info(
7171
self.info["filling-transition-tool"] = t8n_version
7272
self.info["description"] = test_case_description
7373
self.info["url"] = fixture_source_url
74+
self.info["fixture_format"] = self.fixture_format_name
7475
if ref_spec is not None:
7576
ref_spec.write_info(self.info)
7677

Diff for: src/ethereum_test_fixtures/file.py

+41-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
import json
66
from pathlib import Path
7-
from typing import Any, Dict, Optional
7+
from typing import Annotated, Any, Dict, Optional
8+
9+
from pydantic import Discriminator, Tag
810

911
from ethereum_test_base_types import EthereumTestRootModel
1012

@@ -13,8 +15,11 @@
1315
from .blockchain import Fixture as BlockchainFixture
1416
from .eof import Fixture as EOFFixture
1517
from .state import Fixture as StateFixture
18+
from .transaction import Fixture as TransactionFixture
1619

17-
FixtureModel = BlockchainFixture | BlockchainEngineFixture | StateFixture | EOFFixture
20+
FixtureModel = (
21+
BlockchainFixture | BlockchainEngineFixture | StateFixture | EOFFixture | TransactionFixture
22+
)
1823

1924

2025
class BaseFixturesRootModel(EthereumTestRootModel):
@@ -101,6 +106,7 @@ def from_json_data(
101106
BlockchainFixture: BlockchainFixtures,
102107
BlockchainEngineFixture: BlockchainEngineFixtures,
103108
StateFixture: StateFixtures,
109+
TransactionFixture: TransactionFixtures,
104110
EOFFixture: EOFFixtures,
105111
}
106112

@@ -114,12 +120,34 @@ def from_json_data(
114120
return model_class(root=json_data)
115121

116122

123+
def fixture_format_discriminator(v: Any) -> str | None:
124+
"""
125+
A discriminator function that returns the model type as a string.
126+
"""
127+
if v is None:
128+
return None
129+
if isinstance(v, dict):
130+
info_dict = v["_info"]
131+
elif hasattr(v, "info"):
132+
info_dict = v.info
133+
return info_dict.get("fixture_format")
134+
135+
117136
class Fixtures(BaseFixturesRootModel):
118137
"""
119138
A model that can contain any fixture type.
120139
"""
121140

122-
root: Dict[str, BlockchainFixture | BlockchainEngineFixture | StateFixture]
141+
root: Dict[
142+
str,
143+
Annotated[
144+
Annotated[BlockchainFixture, Tag(BlockchainFixture.fixture_format_name)]
145+
| Annotated[BlockchainEngineFixture, Tag(BlockchainEngineFixture.fixture_format_name)]
146+
| Annotated[StateFixture, Tag(StateFixture.fixture_format_name)]
147+
| Annotated[TransactionFixture, Tag(TransactionFixture.fixture_format_name)],
148+
Discriminator(fixture_format_discriminator),
149+
],
150+
]
123151

124152

125153
class BlockchainFixtures(BaseFixturesRootModel):
@@ -152,6 +180,16 @@ class StateFixtures(BaseFixturesRootModel):
152180
root: Dict[str, StateFixture]
153181

154182

183+
class TransactionFixtures(BaseFixturesRootModel):
184+
"""
185+
Defines a top-level model containing multiple transaction test fixtures in a
186+
dictionary of (fixture-name, fixture) pairs. This is the format used in JSON
187+
fixture files for transaction tests.
188+
"""
189+
190+
root: Dict[str, TransactionFixture]
191+
192+
155193
class EOFFixtures(BaseFixturesRootModel):
156194
"""
157195
Defines a top-level model containing multiple state test fixtures in a

Diff for: src/ethereum_test_fixtures/transaction.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
TransactionTest types
3+
"""
4+
5+
from typing import ClassVar, Mapping
6+
7+
from pydantic import Field
8+
9+
from ethereum_test_base_types import Address, Bytes, Hash, ZeroPaddedHexNumber
10+
from ethereum_test_exceptions import TransactionExceptionInstanceOrList
11+
from ethereum_test_types.types import CamelModel
12+
13+
from .base import BaseFixture
14+
15+
16+
class FixtureResult(CamelModel):
17+
"""
18+
The per-network (fork) result structure.
19+
"""
20+
21+
hash: Hash | None = None
22+
intrinsic_gas: ZeroPaddedHexNumber
23+
sender: Address | None = None
24+
exception: TransactionExceptionInstanceOrList | None = None
25+
26+
27+
class Fixture(BaseFixture):
28+
"""
29+
Fixture for a single TransactionTest.
30+
"""
31+
32+
fixture_format_name: ClassVar[str] = "transaction_test"
33+
description: ClassVar[str] = "Tests that generate a transaction test fixture."
34+
35+
result: Mapping[str, FixtureResult]
36+
transaction: Bytes = Field(..., alias="txbytes")
37+
38+
def get_fork(self) -> str | None:
39+
"""
40+
Returns the fork of the fixture as a string.
41+
"""
42+
forks = list(self.result.keys())
43+
assert len(forks) == 1, "Expected transaction test fixture with single fork"
44+
return forks[0]

Diff for: src/ethereum_test_forks/base_fork.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
from abc import ABC, ABCMeta, abstractmethod
6-
from typing import Any, ClassVar, List, Mapping, Optional, Protocol, Tuple, Type
6+
from typing import Any, ClassVar, List, Mapping, Optional, Protocol, Sized, Tuple, Type
77

88
from semver import Version
99

@@ -62,7 +62,7 @@ def __call__(
6262
calldata: BytesConvertible = b"",
6363
contract_creation: bool = False,
6464
access_list: List[AccessList] | None = None,
65-
authorization_count: int | None = None,
65+
authorization_list_or_count: Sized | int | None = None,
6666
) -> int:
6767
"""
6868
Returns the intrinsic gas cost of a transaction given its properties.

0 commit comments

Comments
 (0)