Skip to content

Commit ac615f6

Browse files
feat(fw): implement execute to run tests on live networks (#686)
Co-authored-by: danceratopz <[email protected]>
1 parent 247c312 commit ac615f6

39 files changed

+2926
-151
lines changed

docs/executing_tests/index.md

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Executing Tests on Local Networks or Hive
2+
3+
@ethereum/execution-spec-tests is capable of running tests on local networks or on Hive with a few considerations. This page describes how to do so.
4+
5+
## The `execute` command and `pytest` plugin
6+
7+
The `execute` command is capable of parse and execute all tests in the `tests` directory, collect the transactions it requires, send them to a client connected to a network, wait for the network to include them in a block and, finally, check the resulting state of the involved smart-contracts against the expected state to validate the behavior of the clients.
8+
9+
It will not check for the state of the network itself, only the state of the smart-contracts, accounts and transactions involved in the tests, so it is possible that the network becomes unstable or forks during the execution of the tests, but this will not be detected by the command.
10+
11+
The way this is achieved is by using a pytest plugin that will collect all the tests the same way as the fill plugin does, but instead of compiling the transactions and sending them as a batch to the transition tool, they are prepared and sent to the client one by one.
12+
13+
Before sending the actual test transactions to the client, the plugin uses a special pre-allocation object that collects the contracts and EOAs that are used by the tests and, instead of pre-allocating them in a dictionary as the fill plugin does, it sends transactions to deploy contracts or fund the accounts for them to be available in the network.
14+
15+
The pre-allocation object requires a seed account with funds available in the network to be able to deploy contracts and fund accounts. In the case of a live remote network, the seed account needs to be provided via a command-line parameter, but in the case of a local hive network, the seed account is automatically created and funded by the plugin via the genesis file.
16+
17+
At the end of each test, the plugin will also check the remaining balance of all accounts and will attempt to automatically recover the funds back to the seed account in order to execute the following tests.
18+
19+
## Differences between the `fill` and `execute` plugins
20+
21+
The test execution with the `execute` plugin is different from the `fill` plugin in a few ways:
22+
23+
### EOA and Contract Addresses
24+
25+
The `fill` plugin will pre-allocate all the accounts and contracts that are used in the tests, so the addresses of the accounts and contracts will be known before the tests are executed, Further more, the test contracts will start from the same address on different tests, so there are collisions on the account addresses used across different tests. This is not the case with the `execute` plugin, as the accounts and contracts are deployed on the fly, from sender keys that are randomly generated and therefore are different in each execution.
26+
27+
Reasoning behind the random generation of the sender keys is that one can execute the same test multiple times in the same network and the plugin will not fail because the accounts and contracts are already deployed.
28+
29+
### Transactions Gas Price
30+
31+
The `fill` plugin will use a fixed and minimum gas price for all the transactions it uses for testing, but this is not possible with the `execute` plugin, as the gas price is determined by the current state of the network.
32+
33+
At the moment, the `execute` plugin does not query the client for the current gas price, but instead uses a fixed increment to the gas price in order to avoid the transactions to be stuck in the mempool.
34+
35+
## Running Tests on a Hive Single-Client Local Network
36+
37+
Tests can be executed on a local hive-controlled single-client network by running the `execute hive` command.
38+
39+
This command requires hive to be running in `--dev` mode:
40+
41+
```bash
42+
./hive --dev --client go-ethereum
43+
```
44+
45+
This will start hive in dev mode with the single go-ethereum client available for launching tests.
46+
47+
By default, the hive server will be listening on `http://127.0.0.1:3000`, but this can be changed by setting the `--dev.addr` flag:
48+
49+
```bash
50+
./hive --dev --client go-ethereum --dev.addr http://127.0.0.1:5000
51+
```
52+
53+
The `execute hive` can now be executed to connect to the hive server, but the environment variable `HIVE_SIMULATOR` needs to be set to the address of the hive server:
54+
55+
```bash
56+
export HIVE_SIMULATOR=http://127.0.0.1:3000
57+
```
58+
59+
And the tests can be executed with:
60+
61+
```bash
62+
uv run execute hive --fork=Cancun
63+
```
64+
65+
This will execute all available tests in the `tests` directory on the `Cancun` fork by connecting to the hive server running on `http://127.0.0.1:3000` and launching a single client with the appropriate genesis file.
66+
67+
The genesis file is passed to the client with the appropriate configuration for the fork schedule, system contracts and pre-allocated seed account.
68+
69+
All tests will be executed in the same network, in the same client, and serially, but when the `-n auto` parameter is passed to the command, the tests can also be executed in parallel.
70+
71+
One important feature of the `execute hive` command is that, since there is no consensus client running in the network, the command drives the chain by the use of the Engine API to prompt the execution client to generate new blocks and include the transactions in them.
72+
73+
## Running Test on a Live Remote Network
74+
75+
Tests can be executed on a live remote network by running the `execute remote` command.
76+
77+
The command also accepts the `--fork` flag which should match the fork that is currently active in the network (fork transition tests are not supported yet).
78+
79+
The `execute remote` command requires to be pointed to an RPC endpoint of a client that is connected to the network, which can be specified by using the `--rpc-endpoint` flag:
80+
81+
```bash
82+
uv run execute remote --rpc-endpoint=https://rpc.endpoint.io
83+
```
84+
85+
Another requirement is that the command is provided with a seed account that has funds available in the network to deploy contracts and fund accounts. This can be done by setting the `--rpc-seed-key` flag:
86+
87+
```bash
88+
uv run execute remote --rpc-endpoint=https://rpc.endpoint.io --rpc-seed-key 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
89+
```
90+
91+
The value needs to be a private key that is used to sign the transactions that deploy the contracts and fund the accounts.
92+
93+
One last requirement is that the `--rpc-chain-id` flag is set to the chain id of the network that is being tested:
94+
95+
```bash
96+
uv run execute remote --rpc-endpoint=https://rpc.endpoint.io --rpc-seed-key 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f --rpc-chain-id 12345
97+
```
98+
99+
## `execute` Command Test Execution
100+
101+
After executing wither `execute hive` or `execute remote`, the command will first create a random sender account from which all required test accounts will be deployed and funded, and this account is funded by sweeping (by default) the seed account.
102+
103+
The sweep amount can be configured by setting the `--seed-account-sweep-amount` flag:
104+
105+
```bash
106+
--seed-account-sweep-amount "1000 ether"
107+
```
108+
109+
Once the sender account is funded, the command will start executing tests one by one by sending the transactions from this account to the network.
110+
111+
Test transactions are not sent from the main sender account though, they are sent from a different unique account that is created for each test (accounts returned by `pre.fund_eoa`).
112+
113+
If the command is run using the `-n` flag, the tests will be executed in parallel, and each process will have its own separate sender account, so the amount that is swept from the seed account is divided by the number of processes, so this has to be taken into account when setting the sweep amount and also when funding the seed account.
114+
115+
After finishing each test the command will check the remaining balance of all accounts and will attempt to recover the funds back to the sender account, and at the end of all tests, the remaining balance of the sender account will be swept back to the seed account.
116+
117+
There are instances where it will be impossible to recover the funds back from a test, for example, funds that are sent to a contract that has no built-in way to send them back, the funds will be stuck in the contract and they will not be recoverable.

docs/navigation.md

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* [EOF Tests](consuming_tests/eof_test.md)
2828
* [Common Types](consuming_tests/common_types.md)
2929
* [Exceptions](consuming_tests/exceptions.md)
30+
* [Executing Tests](executing_tests/index.md)
3031
* [Getting Help](getting_help/index.md)
3132
* [Developer Doc](dev/index.md)
3233
* [Managing Configurations](dev/configurations.md)

docs/writing_tests/test_markers.md

+42
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,48 @@ def test_something_with_all_tx_types_but_skip_type_1(state_test_only, tx_type):
271271

272272
In this example, the test will be skipped if `tx_type` is equal to 1 by returning a `pytest.mark.skip` marker, and return `None` otherwise.
273273

274+
## Fill/Execute Markers
275+
276+
These markers are used to apply different markers to a test depending on whether it is being filled or executed.
277+
278+
### `@pytest.mark.fill`
279+
280+
This marker is used to apply markers to a test when it is being filled.
281+
282+
```python
283+
import pytest
284+
285+
from ethereum_test_tools import Alloc, StateTestFiller
286+
287+
@pytest.mark.fill(pytest.mark.skip(reason="Only for execution"))
288+
def test_something(
289+
state_test: StateTestFiller,
290+
pre: Alloc
291+
):
292+
pass
293+
```
294+
295+
In this example, the test will be skipped when it is being filled.
296+
297+
### `@pytest.mark.execute`
298+
299+
This marker is used to apply markers to a test when it is being executed.
300+
301+
```python
302+
import pytest
303+
304+
from ethereum_test_tools import Alloc, StateTestFiller
305+
306+
@pytest.mark.execute(pytest.mark.xfail(reason="Depends on block context"))
307+
def test_something(
308+
state_test: StateTestFiller,
309+
pre: Alloc
310+
):
311+
pass
312+
```
313+
314+
In this example, the test will be marked as expected to fail when it is being executed, which is particularly useful so that the test is still executed but does not fail the test run.
315+
274316
## Other Markers
275317

276318
### `@pytest.mark.slow`

pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies = [
4343
"ethereum-types>=0.2.1,<0.3",
4444
"pyyaml>=6.0.2",
4545
"types-pyyaml>=6.0.12.20240917",
46+
"pytest-json-report>=1.5.0,<2",
4647
]
4748

4849
[project.urls]
@@ -85,6 +86,7 @@ docs = [
8586
[project.scripts]
8687
fill = "cli.pytest_commands.fill:fill"
8788
phil = "cli.pytest_commands.fill:phil"
89+
execute = "cli.pytest_commands.execute:execute"
8890
tf = "cli.pytest_commands.fill:tf"
8991
checkfixtures = "cli.check_fixtures:check_fixtures"
9092
consume = "cli.pytest_commands.consume:consume"

pytest-execute-hive.ini

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[pytest]
2+
console_output_style = count
3+
minversion = 7.0
4+
python_files = *.py
5+
testpaths = tests/
6+
markers =
7+
slow
8+
pre_alloc_modify
9+
addopts =
10+
-p pytest_plugins.concurrency
11+
-p pytest_plugins.execute.sender
12+
-p pytest_plugins.execute.pre_alloc
13+
-p pytest_plugins.solc.solc
14+
-p pytest_plugins.execute.rpc.hive
15+
-p pytest_plugins.execute.execute
16+
-p pytest_plugins.shared.execute_fill
17+
-p pytest_plugins.forks.forks
18+
-p pytest_plugins.spec_version_checker.spec_version_checker
19+
-p pytest_plugins.pytest_hive.pytest_hive
20+
-p pytest_plugins.help.help
21+
-m "not eip_version_check"
22+
--tb short
23+
--dist loadscope
24+
--ignore tests/cancun/eip4844_blobs/point_evaluation_vectors/

pytest-execute-recover.ini

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[pytest]
2+
console_output_style = count
3+
minversion = 7.0
4+
python_files = *.py
5+
testpaths = src/pytest_plugins/execute/test_recover.py
6+
markers =
7+
slow
8+
pre_alloc_modify
9+
addopts =
10+
-p pytest_plugins.execute.rpc.remote
11+
-p pytest_plugins.execute.recover
12+
-p pytest_plugins.help.help
13+
-m "not eip_version_check"
14+
--tb short
15+
--dist loadscope

pytest-execute.ini

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[pytest]
2+
console_output_style = count
3+
minversion = 7.0
4+
python_files = *.py
5+
testpaths = tests/
6+
markers =
7+
slow
8+
pre_alloc_modify
9+
addopts =
10+
-p pytest_plugins.concurrency
11+
-p pytest_plugins.execute.sender
12+
-p pytest_plugins.execute.pre_alloc
13+
-p pytest_plugins.solc.solc
14+
-p pytest_plugins.execute.execute
15+
-p pytest_plugins.shared.execute_fill
16+
-p pytest_plugins.execute.rpc.remote_seed_sender
17+
-p pytest_plugins.execute.rpc.remote
18+
-p pytest_plugins.forks.forks
19+
-p pytest_plugins.spec_version_checker.spec_version_checker
20+
-p pytest_plugins.help.help
21+
-m "not eip_version_check"
22+
--tb short
23+
--dist loadscope
24+
--ignore tests/cancun/eip4844_blobs/point_evaluation_vectors/

pytest-framework.ini

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ addopts =
1414
--ignore=src/pytest_plugins/consume/direct/test_via_direct.py
1515
--ignore=src/pytest_plugins/consume/hive_simulators/engine/test_via_engine.py
1616
--ignore=src/pytest_plugins/consume/hive_simulators/rlp/test_via_rlp.py
17+
--ignore=src/pytest_plugins/execute/test_recover.py

pytest.ini

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ addopts =
1111
-p pytest_plugins.filler.pre_alloc
1212
-p pytest_plugins.solc.solc
1313
-p pytest_plugins.filler.filler
14+
-p pytest_plugins.shared.execute_fill
1415
-p pytest_plugins.forks.forks
1516
-p pytest_plugins.spec_version_checker.spec_version_checker
1617
-p pytest_plugins.eels_resolver

src/cli/pytest_commands/common.py

+31-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Common functions for CLI pytest-based entry points.
33
"""
44

5-
from typing import Any, Callable, List
5+
from typing import Any, Callable, Dict, List
66

77
import click
88

@@ -38,6 +38,31 @@ def common_click_options(func: Callable[..., Any]) -> Decorator:
3838
return click.argument("pytest_args", nargs=-1, type=click.UNPROCESSED)(func)
3939

4040

41+
REQUIRED_FLAGS: Dict[str, List] = {
42+
"fill": [],
43+
"consume": [],
44+
"execute": [
45+
"--rpc-endpoint",
46+
"x",
47+
"--rpc-seed-key",
48+
"x",
49+
"--rpc-chain-id",
50+
"1",
51+
],
52+
"execute-hive": [],
53+
"execute-recover": [
54+
"--rpc-endpoint",
55+
"x",
56+
"--rpc-chain-id",
57+
"1",
58+
"--start-eoa-index",
59+
"1",
60+
"--destination",
61+
"0x1234567890123456789012345678901234567890",
62+
],
63+
}
64+
65+
4166
def handle_help_flags(pytest_args: List[str], pytest_type: str) -> List[str]:
4267
"""
4368
Modifies the help arguments passed to the click CLI command before forwarding to
@@ -49,7 +74,11 @@ def handle_help_flags(pytest_args: List[str], pytest_type: str) -> List[str]:
4974
ctx = click.get_current_context()
5075

5176
if ctx.params.get("help_flag"):
52-
return [f"--{pytest_type}-help"] if pytest_type in {"consume", "fill"} else pytest_args
77+
return (
78+
[f"--{pytest_type}-help", *REQUIRED_FLAGS[pytest_type]]
79+
if pytest_type in {"consume", "fill", "execute", "execute-hive", "execute-recover"}
80+
else pytest_args
81+
)
5382
elif ctx.params.get("pytest_help_flag"):
5483
return ["--help"]
5584

src/cli/pytest_commands/execute.py

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
CLI entry point for the `execute` pytest-based command.
3+
"""
4+
5+
import sys
6+
from typing import Tuple
7+
8+
import click
9+
import pytest
10+
11+
from .common import common_click_options, handle_help_flags
12+
13+
14+
@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
15+
def execute() -> None:
16+
"""
17+
Execute command to run tests in hive or live networks.
18+
"""
19+
pass
20+
21+
22+
@execute.command(context_settings=dict(ignore_unknown_options=True))
23+
@common_click_options
24+
def hive(
25+
pytest_args: Tuple[str, ...],
26+
**kwargs,
27+
) -> None:
28+
"""
29+
Execute tests using hive in dev-mode as backend, requires hive to be running
30+
(using command: `./hive --dev`).
31+
"""
32+
pytest_type = "execute-hive"
33+
args = handle_help_flags(list(pytest_args), pytest_type=pytest_type)
34+
ini_file = "pytest-execute-hive.ini"
35+
args = ["-c", ini_file] + args
36+
result = pytest.main(args)
37+
sys.exit(result)
38+
39+
40+
@execute.command(context_settings=dict(ignore_unknown_options=True))
41+
@common_click_options
42+
def remote(
43+
pytest_args: Tuple[str, ...],
44+
**kwargs,
45+
) -> None:
46+
"""
47+
Execute tests using a remote RPC endpoint.
48+
"""
49+
pytest_type = "execute"
50+
args = handle_help_flags(list(pytest_args), pytest_type=pytest_type)
51+
ini_file = "pytest-execute.ini"
52+
args = ["-c", ini_file] + args
53+
result = pytest.main(args)
54+
sys.exit(result)
55+
56+
57+
@execute.command(context_settings=dict(ignore_unknown_options=True))
58+
@common_click_options
59+
def recover(
60+
pytest_args: Tuple[str, ...],
61+
**kwargs,
62+
) -> None:
63+
"""
64+
Recover funds from a failed test execution using a remote RPC endpoint.
65+
"""
66+
pytest_type = "execute-recover"
67+
args = handle_help_flags(list(pytest_args), pytest_type=pytest_type)
68+
ini_file = "pytest-execute-recover.ini"
69+
args = ["-c", ini_file] + args
70+
result = pytest.main(args)
71+
sys.exit(result)

0 commit comments

Comments
 (0)