From 54b205cfab3c6178e6fa026898e5f2d6064acb63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E4=BD=B3=E8=AA=A0=20Louis=20Tsai?= <72684086+LouisTsai-Csie@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:05:15 +0800 Subject: [PATCH 1/4] refactor(test-benchmark): ensure no precompile cache issue (#2415) --- .../compute/precompile/test_alt_bn128.py | 203 ++++++++++++++++++ .../compute/precompile/test_ecrecover.py | 23 +- 2 files changed, 222 insertions(+), 4 deletions(-) diff --git a/tests/benchmark/compute/precompile/test_alt_bn128.py b/tests/benchmark/compute/precompile/test_alt_bn128.py index c780788c2d3..112bfcbe851 100644 --- a/tests/benchmark/compute/precompile/test_alt_bn128.py +++ b/tests/benchmark/compute/precompile/test_alt_bn128.py @@ -1,15 +1,20 @@ """Benchmark ALT_BN128 precompile.""" +import math import random import pytest from execution_testing import ( Address, + Alloc, BenchmarkTestFiller, + Block, Bytes, Fork, JumpLoopGenerator, Op, + Transaction, + While, ) from py_ecc.bn128 import G1, G2, multiply @@ -522,3 +527,201 @@ def test_alt_bn128_benchmark( tx_kwargs={"data": calldata}, ), ) + + +@pytest.mark.repricing +@pytest.mark.parametrize("num_pairs", [1, 3, 6, 12, 24]) +def test_ec_pairing( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, + fork: Fork, + gas_benchmark_value: int, + tx_gas_limit: int, + num_pairs: int, +) -> None: + """Benchmark ecpairing precompile with unique inputs per call.""" + pair_size = num_pairs * 192 + gsc = fork.gas_costs() + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + mem_exp = fork.memory_expansion_gas_calculator() + precompile_cost = ( + gsc.GAS_PRECOMPILE_ECPAIRING_BASE + + gsc.GAS_PRECOMPILE_ECPAIRING_PER_POINT * num_pairs + ) + + # Each iteration: STATICCALL ecpairing at advancing calldata offset, + # then advance offset by pair_size at memory[CALLDATASIZE]. + # The loop condition checks remaining gas against one body execution. + attack_block = Op.POP( + Op.STATICCALL( + gas=Op.GAS, + address=0x08, + args_offset=Op.MLOAD(Op.CALLDATASIZE), + args_size=pair_size, + address_warm=True, + ), + ) + Op.MSTORE( + Op.CALLDATASIZE, + Op.ADD(Op.MLOAD(Op.CALLDATASIZE), pair_size), + ) + + setup = Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + loop = While( + body=attack_block, + condition=Op.GT(Op.CALLDATASIZE, Op.MLOAD(Op.CALLDATASIZE)), + ) + code = setup + loop + attack_contract_address = pre.deploy_contract(code=code) + + iteration_cost = loop.gas_cost(fork) + precompile_cost + setup_cost = setup.gas_cost(fork) + + # Conservative per-variant estimate for sizing the calldata: + # one loop iteration + worst-case calldata intrinsic (all non-zero) + # + CALLDATACOPY copy and linear memory expansion. + words_per_variant = math.ceil(pair_size / 32) + per_variant_gas = ( + iteration_cost + + pair_size * 16 + + words_per_variant * (gsc.GAS_COPY + gsc.GAS_MEMORY) + ) + empty_intrinsic = intrinsic_gas_calculator( + calldata=[], return_cost_deducted_prior_execution=True + ) + fixed_overhead = empty_intrinsic + setup_cost + mem_exp(new_bytes=32) + + seed_offset = 0 + txs: list[Transaction] = [] + remaining_gas = gas_benchmark_value + + while remaining_gas > 0: + per_tx_gas = min(tx_gas_limit, remaining_gas) + per_tx_variants = max( + 1, (per_tx_gas - fixed_overhead) // per_variant_gas + ) + calldata = Bytes( + b"".join( + _generate_bn128_pairs(num_pairs, seed=42 + seed_offset + i) + for i in range(per_tx_variants) + ) + ) + + execution_intrinsic = intrinsic_gas_calculator( + calldata=calldata, + return_cost_deducted_prior_execution=True, + ) + gas_for_loop = ( + per_tx_gas + - execution_intrinsic + - setup_cost + - math.ceil(len(calldata) / 32) * gsc.GAS_COPY + - mem_exp(new_bytes=len(calldata) + 32) + ) + + if gas_for_loop < iteration_cost: + break + + txs.append( + Transaction( + to=attack_contract_address, + sender=pre.fund_eoa(), + gas_limit=per_tx_gas, + data=calldata, + ) + ) + remaining_gas -= per_tx_gas + seed_offset += per_tx_variants + + benchmark_test( + target_opcode=Op.STATICCALL, + skip_gas_used_validation=True, + blocks=[Block(txs=txs)], + ) + + +def _generate_g1_point(seed: int) -> Bytes: + """Generate a valid random G1 point from a deterministic seed.""" + rng = random.Random(seed) + priv_key = rng.randint(1, 2**32 - 1) + point = multiply(G1, priv_key) + assert point is not None + return Bytes( + point[0].n.to_bytes(32, "big") + point[1].n.to_bytes(32, "big") + ) + + +@pytest.mark.repricing +@pytest.mark.parametrize( + "precompile_address,scalar", + [ + pytest.param(0x06, None, id="ec_add"), + pytest.param(0x07, 2, id="ec_mul_small_scalar"), + pytest.param(0x07, 2**256 - 1, id="ec_mul_max_scalar"), + ], +) +def test_alt_bn128_uncachable( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, + fork: Fork, + gas_benchmark_value: int, + tx_gas_limit: int, + precompile_address: Address, + scalar: int | None, +) -> None: + """ + Benchmark ecAdd/ecMul with unique input per call. + + Write the precompile's G1 output (64 bytes) back over the + input point so each loop iteration receives a distinct + input, avoiding precompile result caching in clients. + """ + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + + attack_block = Op.POP( + Op.STATICCALL( + gas=Op.GAS, + address=precompile_address, + args_size=Op.CALLDATASIZE, + # One G1 point (2 * 32 bytes), overwrites the input point + # so each iteration has unique precompile input. + ret_size=64, + ), + ) + + setup = Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + loop = While(body=attack_block, condition=Op.GAS) + code = setup + loop + attack_contract_address = pre.deploy_contract(code=code) + + txs: list[Transaction] = [] + remaining_gas = gas_benchmark_value + + seed = 0 + while remaining_gas > 0: + gas_available = min(tx_gas_limit, remaining_gas) + + calldata = Bytes( + _generate_g1_point(seed) + _generate_g1_point(seed + 1000) + if scalar is None + else _generate_g1_point(seed) + scalar.to_bytes(32, "big") + ) + + intrinsic = intrinsic_gas_calculator(calldata=calldata) + if gas_available <= intrinsic: + break + + txs.append( + Transaction( + to=attack_contract_address, + sender=pre.fund_eoa(), + gas_limit=gas_available, + data=calldata, + ) + ) + remaining_gas -= gas_available + seed += 1 + + benchmark_test( + target_opcode=Op.STATICCALL, + blocks=[Block(txs=txs)], + ) diff --git a/tests/benchmark/compute/precompile/test_ecrecover.py b/tests/benchmark/compute/precompile/test_ecrecover.py index a3906f721d3..ab8b5cdbf28 100644 --- a/tests/benchmark/compute/precompile/test_ecrecover.py +++ b/tests/benchmark/compute/precompile/test_ecrecover.py @@ -38,13 +38,28 @@ def test_ecrecover( precompile_address: Address, calldata: bytes, ) -> None: - """Benchmark ECRECOVER precompile.""" + """ + Benchmark ECRECOVER precompile with unique input per call. + + Each loop iteration increments the hash at memory[0] by + the STATICCALL success flag (1) so every call receives a + distinct input, avoiding precompile result caching in + clients. + """ if precompile_address not in fork.precompiles(): pytest.skip("Precompile not enabled") - attack_block = Op.POP( - Op.STATICCALL( - gas=Op.GAS, address=precompile_address, args_size=Op.CALLDATASIZE + attack_block = Op.MSTORE( + 0, + Op.ADD( + Op.MLOAD(0), + Op.STATICCALL( + gas=Op.GAS, + address=precompile_address, + args_size=Op.CALLDATASIZE, + ret_offset=0x80, + ret_size=0x20, + ), ), ) From 40badcfd3b42e41de19c32041eea10b6e62e2348 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 19 Mar 2026 10:08:01 +0100 Subject: [PATCH 2/4] fix(benchmarks): correct gas accounting for account_query value > 0 (#2530) --- tests/benchmark/stateful/bloatnet/test_account_query.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/benchmark/stateful/bloatnet/test_account_query.py b/tests/benchmark/stateful/bloatnet/test_account_query.py index ece0cc06d9a..e7d5a47ad38 100644 --- a/tests/benchmark/stateful/bloatnet/test_account_query.py +++ b/tests/benchmark/stateful/bloatnet/test_account_query.py @@ -164,6 +164,7 @@ def test_account_query( # Gas accounting address_warm=access_warm, new_memory_size=max(mem_size, 96), + value_transfer=value_sent > 0, ) ) elif opcode in (Op.STATICCALL, Op.DELEGATECALL): From 9a289233151fe0cd22bc2975bd4b480e1d88b794 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Thu, 19 Mar 2026 11:11:25 +0100 Subject: [PATCH 3/4] chore(ci): warn when local markdownlint-cli2 version diverges from CI (#2513) Co-authored-by: felipe --- .../src/execution_testing/cli/tox_helpers.py | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/testing/src/execution_testing/cli/tox_helpers.py b/packages/testing/src/execution_testing/cli/tox_helpers.py index 752a6a86c2e..cf14af6ee38 100644 --- a/packages/testing/src/execution_testing/cli/tox_helpers.py +++ b/packages/testing/src/execution_testing/cli/tox_helpers.py @@ -14,6 +14,7 @@ from pathlib import Path import click +import semver from pyspelling import __main__ as pyspelling_main # type: ignore from rich.console import Console @@ -85,20 +86,50 @@ def markdownlint(args: tuple[str, ...]) -> None: Allows argument forwarding to markdownlint-cli2. """ + expected_version = "0.20.0" markdownlint = shutil.which("markdownlint-cli2") if not markdownlint: # Note: There's an additional step in test.yaml to run markdownlint- # cli2 in GitHub Actions click.echo( - "********* Install 'markdownlint-cli2' to enable markdown linting\ - *********\ - ```\ - sudo npm install -g markdownlint-cli2@0.20.0\ - ```\ - " + "********* Install 'markdownlint-cli2' to enable markdown linting" + " *********\n" + "```\n" + f"sudo npm install -g markdownlint-cli2@{expected_version}\n" + "```" ) sys.exit(0) + result = subprocess.run( + [markdownlint, "--version"], + capture_output=True, + text=True, + ) + if result.returncode == 0: + version_match = re.search(r"v?(\d+\.\d+\.\d+)", result.stdout) + installed_version = version_match.group(1) if version_match else None + if installed_version: + installed = semver.Version.parse(installed_version) + expected = semver.Version.parse(expected_version) + minor_mismatch = (installed.major, installed.minor) != ( + expected.major, + expected.minor, + ) + else: + minor_mismatch = False + if minor_mismatch: + lines = [ + f"WARNING: markdownlint-cli2 {installed_version} " + f"installed, CI uses {expected_version}", + "", + "Lint results may differ from CI.", + f" npm install -g markdownlint-cli2@{expected_version}", + ] + width = max(len(line) for line in lines) + 4 + border = "*" * width + box = "\n".join(f"* {line:<{width - 4}} *" for line in lines) + click.echo(f"\n{border}\n{box}\n{border}\n") + args_list: list[str] = ( list(args) if len(args) > 0 else ["./docs/**/*.md", "./*.md"] ) @@ -219,7 +250,6 @@ def codespell() -> None: sys.exit(1) sys.exit(0) - sys.exit(pyspelling_main.main()) @click.command() From 3ffa9e780ce0859b8480c20ff0c64c93bbe2744b Mon Sep 17 00:00:00 2001 From: felix Date: Thu, 19 Mar 2026 11:30:11 +0100 Subject: [PATCH 4/4] fix(src): preserve custom chain ID in pre-alloc groups and `extract_config` (#2515) * fix: chainid passed by user is used, not 1 * fix: ci * fix(src): address PR review feedback for custom chain ID flow Use DEFAULT_CHAIN_ID in pre-alloc groups, simplify chain ID reset in the filler plugin, and remove the unused legacy_payload variable from the extract_config regression test. Co-authored-by: raxhvl <10168946+raxhvl@users.noreply.github.com> * fix: ruff * fix: felipe feedback --------- Co-authored-by: raxhvl <10168946+raxhvl@users.noreply.github.com> --- .../execution_testing/cli/extract_config.py | 2 +- .../consume/tests/test_consume_args.py | 4 +- .../pytest_commands/plugins/filler/filler.py | 11 ++- .../plugins/filler/tests/test_benchmarking.py | 4 +- .../plugins/filler/tests/test_filler.py | 4 +- .../filler/tests/test_output_directory.py | 4 +- .../cli/tests/test_extract_config.py | 71 +++++++++++++++++++ .../cli/tests/test_pytest_fill_command.py | 54 ++++++++++++-- .../vector_pytest_fill_command_example.py | 13 ++++ ...r_pytest_fill_command_valid_from_prague.py | 19 +++++ .../fixtures/pre_alloc_groups.py | 8 +++ .../test_types/chain_config_types.py | 4 +- 12 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 packages/testing/src/execution_testing/cli/tests/test_extract_config.py create mode 100644 packages/testing/src/execution_testing/cli/tests/vectors/vector_pytest_fill_command_example.py create mode 100644 packages/testing/src/execution_testing/cli/tests/vectors/vector_pytest_fill_command_valid_from_prague.py diff --git a/packages/testing/src/execution_testing/cli/extract_config.py b/packages/testing/src/execution_testing/cli/extract_config.py index af98b15a9d0..4f7d31d538f 100755 --- a/packages/testing/src/execution_testing/cli/extract_config.py +++ b/packages/testing/src/execution_testing/cli/extract_config.py @@ -182,7 +182,7 @@ def from_fixture(cls, fixture_path: Path) -> Self: return cls( header=pre_alloc_group.genesis, alloc=pre_alloc_group.pre, - chain_id=1, # TODO: PreAllocGroups don't contain chain ID + chain_id=pre_alloc_group.chain_id, fork=pre_alloc_group.fork, ) except ValidationError: diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_consume_args.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_consume_args.py index a601e1353e2..8712beee834 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_consume_args.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/tests/test_consume_args.py @@ -13,7 +13,9 @@ MINIMAL_TEST_CONTENTS = """ from execution_testing import Transaction def test_function(state_test, pre): - tx = Transaction(to=0, gas_limit=21_000, sender=pre.fund_eoa()) + tx = Transaction( + to=0, gas_limit=21_000, sender=pre.fund_eoa() + ).with_signature_and_sender() state_test(pre=pre, post={}, tx=tx) """ diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py index 37e68832918..88d42afe0bd 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py @@ -69,7 +69,10 @@ from execution_testing.specs import BaseTest from execution_testing.specs.base import FillResult, OpMode from execution_testing.test_types import EnvironmentDefaults -from execution_testing.test_types.chain_config_types import ChainConfigDefaults +from execution_testing.test_types.chain_config_types import ( + DEFAULT_CHAIN_ID, + ChainConfigDefaults, +) from execution_testing.tools.utility.versioning import ( generate_github_url, get_current_commit_hash_or_tag, @@ -939,15 +942,16 @@ def pytest_configure(config: pytest.Config) -> None: TransitionToolCacheStats() ) + # Default chain id can be overwritten by user flag or env var + ChainConfigDefaults.chain_id = DEFAULT_CHAIN_ID chain_id = config.getoption("chain_id") - if chain_id is None: env_chain_id = os.environ.get("CHAIN_ID") if env_chain_id is not None: chain_id = int(env_chain_id) if chain_id is not None: - ChainConfigDefaults.chain_id = chain_id + ChainConfigDefaults.chain_id = int(chain_id) @pytest.hookimpl(trylast=True) @@ -1714,6 +1718,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: pre_alloc_hash=pre_alloc_hash, test_id=test_id, fork=fork, + chain_id=ChainConfigDefaults.chain_id, environment=genesis_environment, pre=pre, ) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py index be73aa13da9..25c001a4d5c 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py @@ -1147,7 +1147,9 @@ def test_consensus_fixtures_split_by_fork( @pytest.mark.valid_from("Prague") def test_fork_split_example(state_test, pre) -> None: - tx = Transaction(to=0, gas_limit=21_000, sender=pre.fund_eoa()) + tx = Transaction( + to=0, gas_limit=21_000, sender=pre.fund_eoa() + ).with_signature_and_sender() state_test(pre=pre, post={}, tx=tx) """ ) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_filler.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_filler.py index dc9c73dc2dc..22280e6b0fe 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_filler.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_filler.py @@ -765,7 +765,9 @@ def test_fixture_output_based_on_command_line_args( def test_max_gas_limit(state_test, pre, block_gas_limit) -> None: env = Environment() assert block_gas_limit == {expected_gas_limit} - tx = Transaction(gas_limit=block_gas_limit, sender=pre.fund_eoa()) + tx = Transaction( + gas_limit=block_gas_limit, sender=pre.fund_eoa() + ).with_signature_and_sender() state_test(env=env, pre=pre, post={{}}, tx=tx) """ ) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_output_directory.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_output_directory.py index e70e621fc9e..001f489b606 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_output_directory.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_output_directory.py @@ -13,7 +13,9 @@ MINIMAL_TEST_CONTENTS = """ from execution_testing import Transaction def test_function(state_test, pre) -> None: - tx = Transaction(to=0, gas_limit=21_000, sender=pre.fund_eoa()) + tx = Transaction( + to=0, gas_limit=21_000, sender=pre.fund_eoa() + ).with_signature_and_sender() state_test(pre=pre, post={}, tx=tx) """ diff --git a/packages/testing/src/execution_testing/cli/tests/test_extract_config.py b/packages/testing/src/execution_testing/cli/tests/test_extract_config.py new file mode 100644 index 00000000000..c150f43688c --- /dev/null +++ b/packages/testing/src/execution_testing/cli/tests/test_extract_config.py @@ -0,0 +1,71 @@ +"""Tests for the extract_config CLI helpers.""" + +from pathlib import Path + +import pytest + +from execution_testing.base_types import Alloc +from execution_testing.cli.extract_config import GenesisState +from execution_testing.fixtures.pre_alloc_groups import PreAllocGroupBuilder +from execution_testing.forks import ( + Fork, + Prague, + forks_from_until, + get_deployed_forks, +) +from execution_testing.test_types import Environment + + +def forks_from_prague_onward() -> list[Fork]: + """Return deployed forks from Prague onward.""" + all_forks = get_deployed_forks() + return list(forks_from_until(Prague, all_forks[-1])) + + +@pytest.mark.parametrize("fork", forks_from_prague_onward()) +def test_genesis_state_from_pre_alloc_group_uses_stored_chain_id( + tmp_path: Path, + fork: Fork, +) -> None: + """Pre-alloc group files should preserve the configured chain ID.""" + builder = PreAllocGroupBuilder( + test_ids=["test_id"], + environment=Environment() + .set_fork_requirements(fork) + .model_dump(mode="json", exclude={"parent_hash"}), + fork=fork.name(), + chain_id=12345, + pre=Alloc().model_dump(mode="json"), + ) + fixture_path = tmp_path / "pre_alloc.json" + fixture_path.write_text( + builder.model_dump_json(by_alias=True, exclude_none=True, indent=2) + ) + + genesis_state = GenesisState.from_fixture(fixture_path) + + assert genesis_state.chain_id == 12345 + assert genesis_state.get_client_environment()["HIVE_CHAIN_ID"] == "12345" + + +@pytest.mark.parametrize("fork", forks_from_prague_onward()) +def test_genesis_state_from_legacy_pre_alloc_group_defaults_chain_id( + tmp_path: Path, + fork: Fork, +) -> None: + """Legacy pre-alloc groups without chain ID should still default to 1.""" + builder = PreAllocGroupBuilder( + test_ids=["test_id"], + environment=Environment() + .set_fork_requirements(fork) + .model_dump(mode="json", exclude={"parent_hash"}), + fork=fork.name(), + pre=Alloc().model_dump(mode="json"), + ) + fixture_path = tmp_path / "legacy_pre_alloc.json" + fixture_path.write_text(builder.model_dump_json(exclude={"chain_id"})) + + genesis_state = GenesisState.from_fixture(fixture_path) + + assert genesis_state.chain_id == 1 + assert genesis_state.get_client_environment()["HIVE_CHAIN_ID"] == "1" diff --git a/packages/testing/src/execution_testing/cli/tests/test_pytest_fill_command.py b/packages/testing/src/execution_testing/cli/tests/test_pytest_fill_command.py index c663936810b..eb16675996f 100644 --- a/packages/testing/src/execution_testing/cli/tests/test_pytest_fill_command.py +++ b/packages/testing/src/execution_testing/cli/tests/test_pytest_fill_command.py @@ -1,5 +1,7 @@ """Tests for pytest commands (e.g., fill) click CLI.""" +import json +import shutil from pathlib import Path from typing import Callable @@ -12,12 +14,11 @@ from ..pytest_commands.fill import fill MINIMAL_TEST_FILE_NAME = "test_example.py" -MINIMAL_TEST_CONTENTS = """ -from execution_testing import Transaction -def test_function(state_test, pre): - tx = Transaction(to=0, gas_limit=21_000, sender=pre.fund_eoa()) - state_test(pre=pre, post={}, tx=tx) -""" +VECTORS_DIR = Path(__file__).parent / "vectors" +MINIMAL_TEST_SOURCE = VECTORS_DIR / "vector_pytest_fill_command_example.py" +VALID_FROM_PRAGUE_BLOCKCHAIN_TEST_SOURCE = ( + VECTORS_DIR / "vector_pytest_fill_command_valid_from_prague.py" +) @pytest.fixture @@ -102,7 +103,7 @@ def minimal_test_path(self, pytester: pytest.Pytester) -> Path: """ tests_dir = pytester.mkdir("tests") test_file = tests_dir / MINIMAL_TEST_FILE_NAME - test_file.write_text(MINIMAL_TEST_CONTENTS) + shutil.copyfile(MINIMAL_TEST_SOURCE, test_file) return test_file @pytest.fixture @@ -208,6 +209,45 @@ def test_fill_no_html_option( run_fill(*fill_args) assert not default_html_path.exists() + def test_generate_pre_alloc_groups_preserves_chain_id_for_valid_from( + self, + pytester: Pytester, + default_fixtures_output: Path, + ) -> None: + """ + Custom chain ID should be preserved for valid_from-selected forks. + """ + tests_dir = pytester.mkdir("tests") + prague_tests_dir = tests_dir / "prague" + prague_tests_dir.mkdir() + test_file = prague_tests_dir / "test_chain_id_pre_alloc.py" + shutil.copyfile(VALID_FROM_PRAGUE_BLOCKCHAIN_TEST_SOURCE, test_file) + + pytester.copy_example( + name="src/execution_testing/cli/pytest_commands/pytest_ini_files/pytest-fill.ini" + ) + result = pytester.runpytest( + "-c", + "pytest-fill.ini", + "--generate-pre-alloc-groups", + "--chain-id", + "12345", + f"--output={default_fixtures_output}", + str(test_file), + "-q", + ) + assert result.ret == pytest.ExitCode.OK, "\n".join(result.outlines) + + pre_alloc_dir = ( + default_fixtures_output / "blockchain_tests_engine_x" / "pre_alloc" + ) + pre_alloc_files = sorted(pre_alloc_dir.glob("*.json")) + assert pre_alloc_files, f"No pre-alloc files found in {pre_alloc_dir}" + + for pre_alloc_file in pre_alloc_files: + payload = json.loads(pre_alloc_file.read_text()) + assert payload["chainId"] == 12345, pre_alloc_file + def test_fill_html_option( self, run_fill: Callable[..., RunResult], diff --git a/packages/testing/src/execution_testing/cli/tests/vectors/vector_pytest_fill_command_example.py b/packages/testing/src/execution_testing/cli/tests/vectors/vector_pytest_fill_command_example.py new file mode 100644 index 00000000000..86cc4b2140d --- /dev/null +++ b/packages/testing/src/execution_testing/cli/tests/vectors/vector_pytest_fill_command_example.py @@ -0,0 +1,13 @@ +"""Vector file for pytest fill command example coverage.""" + +from typing import Any + +from execution_testing import Transaction + + +def test_function(state_test: Any, pre: Any) -> None: + """Generate a minimal signed state test transaction.""" + tx = Transaction( + to=0, gas_limit=21_000, sender=pre.fund_eoa() + ).with_signature_and_sender() + state_test(pre=pre, post={}, tx=tx) diff --git a/packages/testing/src/execution_testing/cli/tests/vectors/vector_pytest_fill_command_valid_from_prague.py b/packages/testing/src/execution_testing/cli/tests/vectors/vector_pytest_fill_command_valid_from_prague.py new file mode 100644 index 00000000000..73cfd6402b2 --- /dev/null +++ b/packages/testing/src/execution_testing/cli/tests/vectors/vector_pytest_fill_command_valid_from_prague.py @@ -0,0 +1,19 @@ +"""Vector file for valid_from-based pre-alloc group coverage.""" + +from typing import Any + +import pytest + +from execution_testing import Account, Block, TestAddress, Transaction + +TEST_ADDRESS = Account(balance=1_000_000) + + +@pytest.mark.valid_from("Prague") +def test_chain_id_pre_alloc(blockchain_test: Any) -> None: + """Generate a blockchain test selected by the valid_from marker.""" + blockchain_test( + pre={TestAddress: TEST_ADDRESS}, + post={}, + blocks=[Block(txs=[Transaction()])], + ) diff --git a/packages/testing/src/execution_testing/fixtures/pre_alloc_groups.py b/packages/testing/src/execution_testing/fixtures/pre_alloc_groups.py index 69e69fcf21d..34622629bf5 100644 --- a/packages/testing/src/execution_testing/fixtures/pre_alloc_groups.py +++ b/packages/testing/src/execution_testing/fixtures/pre_alloc_groups.py @@ -26,6 +26,7 @@ ) from execution_testing.forks import Fork from execution_testing.test_types import Alloc, Environment +from execution_testing.test_types.chain_config_types import DEFAULT_CHAIN_ID from .blockchain import FixtureHeader @@ -38,6 +39,7 @@ class PreAllocGroupBuilder(CamelModel): ..., description="Grouping environment for this test group" ) fork: Fork = Field(..., alias="network") + chain_id: int = DEFAULT_CHAIN_ID pre: Alloc def get_pre_account_count(self) -> int: @@ -71,6 +73,7 @@ def build(self) -> "PreAllocGroup": test_ids=self.test_ids, environment=self.environment, fork=self.fork, + chain_id=self.chain_id, pre=self.pre.model_dump(), pre_account_count=self.get_pre_account_count(), test_count=self.get_test_count(), @@ -206,6 +209,7 @@ def add_test_pre( pre_alloc_hash: str, test_id: str, fork: Fork, + chain_id: int, environment: Environment, pre: Alloc, ) -> None: @@ -216,6 +220,9 @@ def add_test_pre( assert group.fork == fork, ( f"Incompatible fork: {group.fork}!={fork}" ) + assert group.chain_id == chain_id, ( + f"Incompatible chain id: {group.chain_id}!={chain_id}" + ) group.add_test_alloc(test_id, pre) else: # Create new group - use Environment instead of expensive genesis @@ -223,6 +230,7 @@ def add_test_pre( group = PreAllocGroupBuilder( test_ids=[test_id], fork=fork, + chain_id=chain_id, environment=environment, pre=Alloc.merge( Alloc.model_validate(fork.pre_allocation_blockchain()), diff --git a/packages/testing/src/execution_testing/test_types/chain_config_types.py b/packages/testing/src/execution_testing/test_types/chain_config_types.py index 2e52cb024d4..1f98fc8214f 100644 --- a/packages/testing/src/execution_testing/test_types/chain_config_types.py +++ b/packages/testing/src/execution_testing/test_types/chain_config_types.py @@ -4,6 +4,8 @@ from execution_testing.base_types import CamelModel +DEFAULT_CHAIN_ID = 1 + class ChainConfigDefaults: """ @@ -13,7 +15,7 @@ class ChainConfigDefaults: default values. """ - chain_id: int = 1 + chain_id: int = DEFAULT_CHAIN_ID class ChainConfig(CamelModel):