From d19822166524b988556274907a73055aea93bc69 Mon Sep 17 00:00:00 2001 From: LouisTsai-Csie Date: Wed, 27 May 2026 07:21:37 +0000 Subject: [PATCH 1/6] feat: repleace debug setHead by FCU update --- .../execute/rpc/chain_builder_eth_rpc.py | 27 +++++++++++++++ .../plugins/fill_stateful/fill_stateful.py | 33 +++++++------------ 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py index 66fc5c75e80..53b27a4fca8 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py @@ -347,3 +347,30 @@ def fund_via_withdrawals( new_payload, payload_attributes.parent_beacon_block_root, ) + + def set_canonical_head(self, head_block_hash: Hash) -> None: + """ + Reorg the canonical head to head_block_hash via + ``engine_forkchoiceUpdated``. + """ + head_block = self.get_block_by_hash(head_block_hash) + assert head_block is not None, ( + f"cannot reset head to unknown block {head_block_hash}" + ) + head_fork = self.fork.fork_at( + block_number=HexNumber(head_block["number"]), + timestamp=HexNumber(head_block["timestamp"]), + ) + fcu_version = head_fork.engine_forkchoice_updated_version() + assert fcu_version is not None, ( + "Fork does not support engine forkchoice_updated" + ) + response = self.engine_rpc.forkchoice_updated( + ForkchoiceState(head_block_hash=head_block_hash), + None, + version=fcu_version, + ) + assert response.payload_status.status == PayloadStatusEnum.VALID, ( + f"forkchoice_updated reset to {head_block_hash} was not VALID " + f"(got {response.payload_status.status})" + ) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py index 0d50c671e6a..38579872248 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py @@ -41,7 +41,7 @@ TransitionFork, ) from execution_testing.logging import get_logger -from execution_testing.rpc import DebugRPC, EngineRPC, EthRPC +from execution_testing.rpc import EngineRPC, EthRPC from execution_testing.specs.blockchain import ( payload_metadata_to_fixture, ) @@ -444,12 +444,6 @@ def max_gas_limit_per_test( # --------------------------------------------------------------------------- -@pytest.fixture(scope="session") -def debug_rpc(eth_rpc: EthRPC) -> DebugRPC: - """DebugRPC on the same endpoint as eth_rpc (for debug_setHead).""" - return DebugRPC(eth_rpc.url) - - @pytest.fixture(scope="session") def client_backend( eth_rpc: ChainBuilderEthRPC, @@ -693,35 +687,32 @@ def t8n( @pytest.fixture(autouse=True, scope="function") def _reset_chain_between_tests( client_backend: ClientBackend, - debug_rpc: DebugRPC, eth_rpc: "ChainBuilderEthRPC", ) -> Generator[None, None, None]: """ - Rewind to start_block after each test so the chain is identical for - every fill. ``debug_setHead`` only takes a number, so after the - rewind we re-fetch ``latest`` and fail loudly if the hash drifted - (e.g. live reorg of a same-numbered block). + # Rewind to start_block after each test via engine_forkchoiceUpdated. + # Re-fetch latest and fail if hash drifted (detects live reorgs). """ yield if client_backend.start_block is None: return - start_hex = client_backend.start_block["number"] expected_hash = client_backend.start_block["hash"] - # Skip when head already at start (geth rejects same-block setHead). + current_head = eth_rpc.get_block_by_number("latest") if current_head is not None and current_head["hash"] == expected_hash: return try: - debug_rpc.set_head(start_hex) + eth_rpc.set_canonical_head(Hash(expected_hash)) except Exception as e: - pytest.exit(f"debug_setHead failed — subsequent fixtures invalid: {e}") + pytest.exit( + f"forkchoice reset failed, subsequent fixtures invalid: {e}" + ) head = eth_rpc.get_block_by_number("latest") if head is None or head["hash"] != expected_hash: observed = head["hash"] if head is not None else "" pytest.exit( - f"debug_setHead landed on hash {observed} but expected " - f"{expected_hash} (start_block at number {start_hex}). The " - "live chain may have reorged out from under fill-stateful; " - "rerun against a quiescent client or use --snapshot-block " - "with an explicit hash." + f"forkchoice reset landed on hash {observed} but expected " + f"{expected_hash}. The live chain may have reorged out from " + "under fill-stateful; rerun against a quiescent client or use " + "--snapshot-block with an explicit hash." ) From 0252bec93f69e85d35db564bc7493f28b2948268 Mon Sep 17 00:00:00 2001 From: LouisTsai-Csie Date: Wed, 27 May 2026 07:40:34 +0000 Subject: [PATCH 2/6] feat: increase gas limit via empty blocks --- .../execute/rpc/chain_builder_eth_rpc.py | 35 ++++++++++++ .../plugins/fill_stateful/fill_stateful.py | 54 ++++++++++++------- 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py index 53b27a4fca8..61d30bdd146 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py @@ -348,6 +348,41 @@ def fund_via_withdrawals( payload_attributes.parent_beacon_block_root, ) + def bump_block_gas_limit( + self, + block_count: int, + ) -> List[EnginePayloadMetadata]: + """ + Build empty block to increase the block gas limit to target. + """ + if block_count <= 0: + return [] + assert self.testing_rpc is not None, ( + "bump_block_gas_limit requires testing_rpc" + ) + captured: List[EnginePayloadMetadata] = [] + with self.block_building_lock: + for _ in range(block_count): + head_block = self.get_block_by_number("latest") + assert head_block is not None + next_timestamp = int(HexNumber(head_block["timestamp"]) + 1) + payload_attributes = self._payload_attributes( + next_timestamp=next_timestamp, + ) + new_payload = self.testing_rpc.build_block( + parent_block_hash=Hash(head_block["hash"]), + payload_attributes=payload_attributes, + transactions=[], + extra_data=Bytes(b""), + ) + captured.append( + self._finalize_payload( + new_payload, + payload_attributes.parent_beacon_block_root, + ) + ) + return captured + def set_canonical_head(self, head_block_hash: Hash) -> None: """ Reorg the canonical head to head_block_hash via diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py index 38579872248..55cb4d4820c 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py @@ -117,6 +117,18 @@ def pytest_addoption(parser: pytest.Parser) -> None: "key is generated and funded via CL withdrawal if omitted." ), ) + group.addoption( + "--gas-bump-blocks", + action="store", + dest="gas_bump_blocks", + default=5000, + type=int, + help=( + "Empty blocks at start to increase block gas limit." + "Each block increase ≤ parent_gas_limit/1024." + "Default: 5000 blocks." + ), + ) group.addoption( "--snapshot-block", action="store", @@ -562,32 +574,36 @@ def _session_pre_run( request: pytest.FixtureRequest, ) -> None: """ - Session pre-run: capture snapshot, fund seed, deploy factory, capture - start block, write ``pre_run/.json``. - - The file is named after the start block hash so per-test - ``BlockchainEngineStatefulFixture`` instances reference their setup - file implicitly via their own ``start_block_hash`` field — leaves - room for multiple pre-run files (e.g. different setup variants off - one snapshot) without coordinating filenames. - - Pre-run helpers return their built ``EnginePayloadMetadata`` directly; - we collect them into ``captured`` and serialise via - ``payload_metadata_to_fixture``. + # 1. Snapshot anchor via client-returned hash (survives reorg). + # 2. Ramp gas limit with empty blocks before funding/setup. + # 3. Fund seed key via CL withdrawal. + # 4. Deploy deterministic factory if needed. + # 5. Capture start block (head after setup). + # 6. Write to pre_run/.json for implicit lookup. """ if is_help_or_collectonly_mode(request.config): return - # 1. Snapshot anchor. Either way, the client-returned hash is what we - # record — a later reorg can't silently re-anchor by block number. + # Anchor snapshot (client hash, reorg-safe). client_backend.snapshot_block = snapshot_block logger.info( f"Snapshot block {snapshot_block['number']} " f"hash={snapshot_block['hash'][:20]}..." ) - # 2. Fund seed key via CL withdrawal; helper returns the built payload. captured: List[EnginePayloadMetadata] = [] + + # Ramp gas limit (empty blocks) via empty blocks. + bump_payloads = eth_rpc.bump_block_gas_limit( + request.config.getoption("gas_bump_blocks") + ) + captured.extend(bump_payloads) + if bump_payloads: + logger.info( + f"Ramped block gas limit with {len(bump_payloads)} empty blocks" + ) + + # 3. Fund seed key via CL withdrawal. fund_payload = eth_rpc.fund_via_withdrawals( [(Address(session_worker_key), SEED_FUNDING_WEI)] ) @@ -595,7 +611,7 @@ def _session_pre_run( captured.append(fund_payload) logger.info(f"Funded {Address(session_worker_key)} via withdrawal") - # 3. Deploy deterministic factory if not already present. + # 4. Deploy deterministic factory if needed. lock_file = session_temp_folder / "fill_stateful_setup.lock" with FileLock(lock_file): if ( @@ -614,7 +630,7 @@ def _session_pre_run( ) captured.extend(deploy_payloads) - # 4. Capture start block (head after global setup). + # 5. Capture start block. start_block = eth_rpc.get_block_by_number("latest") assert start_block is not None, "Failed to fetch start block" client_backend.start_block = start_block @@ -623,9 +639,7 @@ def _session_pre_run( f"hash={start_block['hash'][:20]}..." ) - # 5. Persist captured payloads to pre_run/.json. - # Per-test fixtures already carry start_block_hash; naming the - # pre-run file after that hash makes lookup a direct path build. + # Write pre_run/.json. if captured: output_dir = Path(request.config.getoption("output")) pre_run_dir = ( From 05f3c16b2af478e5a87701aa3a732efb9d15f2d4 Mon Sep 17 00:00:00 2001 From: LouisTsai-Csie Date: Wed, 27 May 2026 08:36:19 +0000 Subject: [PATCH 3/6] feat: deterministic sender pool allocation --- .../plugins/fill_stateful/fill_stateful.py | 40 +++++++++++++++---- .../bloatnet/test_transaction_types.py | 5 ++- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py index 55cb4d4820c..bf8e8112478 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py @@ -16,10 +16,11 @@ import os import secrets from pathlib import Path -from typing import Any, Generator, List +from typing import Any, Generator, List, Tuple from urllib.parse import urlparse, urlunparse import pytest +from ethereum.crypto.hash import keccak256 from filelock import FileLock from execution_testing.base_types import ( @@ -61,10 +62,16 @@ from ..shared.helpers import is_help_or_collectonly_mode from ..shared.live_client_flags import FEE_BUMP_MULTIPLIER -# 1 billion ETH. Withdrawals are Gwei (u64-capped); much higher risks -# overflow on some clients, this is plenty for any single fill session. +# 1B ETH for seed account SEED_FUNDING_WEI = 10**9 * 10**18 +# 1 ETH per deterministic sender-pool account. +POOL_FUNDING_WEI = 10**18 + +SENDER_BASE_KEY = int.from_bytes( + keccak256(b"gas-repricings-private-key"), "big" +) + logger = get_logger(__name__) @@ -129,6 +136,17 @@ def pytest_addoption(parser: pytest.Parser) -> None: "Default: 5000 blocks." ), ) + group.addoption( + "--sender-pool-size", + action="store", + dest="sender_pool_size", + default=15000, + type=int, + help=( + "Sender-pool accounts to fund via CL withdrawals for 1 ETH" + "Default: 15000." + ), + ) group.addoption( "--snapshot-block", action="store", @@ -603,13 +621,19 @@ def _session_pre_run( f"Ramped block gas limit with {len(bump_payloads)} empty blocks" ) - # 3. Fund seed key via CL withdrawal. - fund_payload = eth_rpc.fund_via_withdrawals( - [(Address(session_worker_key), SEED_FUNDING_WEI)] - ) + # Fund the seed key and the deterministic sender pool + pool_size = request.config.getoption("sender_pool_size") + funding_targets: List[Tuple[Address, int]] = [ + (Address(session_worker_key), SEED_FUNDING_WEI) + ] + funding_targets += [ + (Address(EOA(key=SENDER_BASE_KEY + i)), POOL_FUNDING_WEI) + for i in range(pool_size) + ] + fund_payload = eth_rpc.fund_via_withdrawals(funding_targets) if fund_payload is not None: captured.append(fund_payload) - logger.info(f"Funded {Address(session_worker_key)} via withdrawal") + logger.info(f"Funded seed key and {pool_size} sender pool accounts") # 4. Deploy deterministic factory if needed. lock_file = session_temp_folder / "fill_stateful_setup.lock" diff --git a/tests/benchmark/stateful/bloatnet/test_transaction_types.py b/tests/benchmark/stateful/bloatnet/test_transaction_types.py index 875c69fecdf..171c12a9e08 100644 --- a/tests/benchmark/stateful/bloatnet/test_transaction_types.py +++ b/tests/benchmark/stateful/bloatnet/test_transaction_types.py @@ -16,13 +16,14 @@ Transaction, compute_create2_address, compute_create_address, + keccak256, ) # Deterministic sender pool of 15K accounts. # Funded via system contract withdrawals (funding.txt) in payload generation. # Placed outside pre-allocation to ensure accounts remain uncached. -SENDER_BASE_KEY = ( - 0x1111111111111111111111111111111111111111111111111111111111111111 +SENDER_BASE_KEY = int.from_bytes( + keccak256(b"gas-repricings-private-key"), "big" ) From 54f43ad277880bf82f0e9c506249df2b6ef5534c Mon Sep 17 00:00:00 2001 From: LouisTsai-Csie Date: Wed, 27 May 2026 14:04:57 +0000 Subject: [PATCH 4/6] refactor: batch receipt request --- .../client_clis/client_backend.py | 8 ++++--- .../testing/src/execution_testing/rpc/rpc.py | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/testing/src/execution_testing/client_clis/client_backend.py b/packages/testing/src/execution_testing/client_clis/client_backend.py index eebd02d278d..3d8260645af 100644 --- a/packages/testing/src/execution_testing/client_clis/client_backend.py +++ b/packages/testing/src/execution_testing/client_clis/client_backend.py @@ -281,10 +281,12 @@ def _finalize( def _fetch_receipts( self, txs: List[Transaction] ) -> List[TransactionReceipt]: - """Fetch receipts for each transaction. TODO: batch via JSON-RPC.""" + """Fetch receipts for all block txs in one JSON-RPC batch.""" + receipts_data = self.eth_rpc.get_transaction_receipts( + [tx.hash for tx in txs] + ) receipts: List[TransactionReceipt] = [] - for tx in txs: - receipt_data = self.eth_rpc.get_transaction_receipt(tx.hash) + for tx, receipt_data in zip(txs, receipts_data, strict=True): if receipt_data is None: raise RuntimeError( f"No receipt found for transaction {tx.hash}" diff --git a/packages/testing/src/execution_testing/rpc/rpc.py b/packages/testing/src/execution_testing/rpc/rpc.py index 2c8cdcb50b5..a647ed7a3bf 100644 --- a/packages/testing/src/execution_testing/rpc/rpc.py +++ b/packages/testing/src/execution_testing/rpc/rpc.py @@ -764,6 +764,30 @@ def get_transaction_receipt( ) ).result_or_raise() + def get_transaction_receipts( + self, transaction_hashes: Sequence[Hash] + ) -> List[dict[str, Any] | None]: + """ + Get transaction receipts for a list of transaction hashes. + Use batch requests to avoid RPC overload. + """ + if not transaction_hashes: + return [] + receipts: List[dict[str, Any] | None] = [] + batch_size = self.max_transactions_per_batch + for i in range(0, len(transaction_hashes), batch_size): + chunk = transaction_hashes[i : i + batch_size] + calls = [ + RPCCall( + method="getTransactionReceipt", + params=[f"{tx_hash}"], + ) + for tx_hash in chunk + ] + responses = self.post_batch_request(calls=calls) + receipts.extend(r.result_or_raise() for r in responses) + return receipts + def get_storage_at( self, address: Address, From 8477bfe48b8eaeebd7727bf0efc5d3626ce40d2e Mon Sep 17 00:00:00 2001 From: LouisTsai-Csie Date: Wed, 27 May 2026 14:40:04 +0000 Subject: [PATCH 5/6] refactor: split large fill-stateful file --- .../plugins/fill_stateful/fill_stateful.py | 176 +---------------- .../plugins/fill_stateful/hive_session.py | 180 ++++++++++++++++++ 2 files changed, 184 insertions(+), 172 deletions(-) create mode 100644 packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/hive_session.py diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py index bf8e8112478..697018a5a2d 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py @@ -37,30 +37,20 @@ ) from execution_testing.forks import ( Fork, - ForkSetAdapter, - InvalidForkError, TransitionFork, ) from execution_testing.logging import get_logger -from execution_testing.rpc import EngineRPC, EthRPC +from execution_testing.rpc import EthRPC from execution_testing.specs.blockchain import ( payload_metadata_to_fixture, ) from execution_testing.test_types import EOA -from execution_testing.test_types.chain_config_types import ( - ChainConfigDefaults, -) from ..execute import contracts from ..execute.rpc.chain_builder_eth_rpc import ChainBuilderEthRPC -from ..execute.rpc.hive import ( - build_client_files, - build_client_genesis_dict, - build_genesis_header, - build_hive_environment, -) from ..shared.helpers import is_help_or_collectonly_mode from ..shared.live_client_flags import FEE_BUMP_MULTIPLIER +from .hive_session import configure_hive, teardown_hive # 1B ETH for seed account SEED_FUNDING_WEI = 10**9 * 10**18 @@ -163,122 +153,6 @@ def pytest_addoption(parser: pytest.Parser) -> None: ) -def _resolve_session_fork( - config: pytest.Config, -) -> Fork | TransitionFork: - """ - Resolve the single fork from ``--fork`` in hive mode. - - Runs in ``pytest_configure`` before forks.py populates - ``selected_fork_set``, so we can't use ``session_fork`` here — the - hive genesis/ruleset must be built before remote.py's pytest_configure - pings the (not-yet-started) client. - """ - fork_arg = config.getoption("single_fork", default="") - if not fork_arg: - pytest.exit( - "--fork is required in --hive-mode (single-fork only).", - returncode=pytest.ExitCode.USAGE_ERROR, - ) - try: - fork_set = ForkSetAdapter.validate_python(fork_arg) - except InvalidForkError: - pytest.exit( - f"Unsupported fork provided to --fork: {fork_arg!r}", - returncode=pytest.ExitCode.USAGE_ERROR, - ) - if len(fork_set) != 1: - pytest.exit( - f"Expected exactly one fork in --hive-mode, got {len(fork_set)} " - f"({sorted(f.name() for f in fork_set)}).", - returncode=pytest.ExitCode.USAGE_ERROR, - ) - return next(iter(fork_set)) - - -def _hive_chain_id(config: pytest.Config) -> int: - """Resolve chain id from CLI/env, defaulting to ChainConfigDefaults.""" - cli_value = config.getoption("chain_id", default=None) - if cli_value is not None: - return int(cli_value) - env_value = os.environ.get("CHAIN_ID") - if env_value is not None: - return int(env_value) - return ChainConfigDefaults.chain_id - - -def _configure_hive(config: pytest.Config) -> None: - """ - Start a hive simulator session and a single execution client, then - rewrite ``config.option`` so the rest of the plugin stack (remote.py - in particular) connects to the hive-managed client. - """ - hive_simulator_url = config.getoption("hive_simulator") - if hive_simulator_url is None: - pytest.exit( - "The HIVE_SIMULATOR environment variable is not set.\n\n" - "If running locally, start hive in --dev mode, for example:\n" - "./hive --dev --client go-ethereum\n\n" - "and set the HIVE_SIMULATOR to the reported URL. For example, " - "in bash:\n" - "export HIVE_SIMULATOR=http://127.0.0.1:3000\n" - "or in fish:\n" - "set -x HIVE_SIMULATOR http://127.0.0.1:3000" - ) - from hive.simulation import Simulation - - session_fork = _resolve_session_fork(config) - chain_id = _hive_chain_id(config) - - simulator = Simulation(url=hive_simulator_url) - suite = simulator.start_suite( - name="fill-stateful test suite", - description="Test suite used to drive a fill-stateful session", - ) - config.hive_test_suite = suite # type: ignore[attr-defined] - base_test = suite.start_test( - name="fill-stateful base test", - description=( - "Base test in the fill-stateful suite hosting the long-lived " - "execution client." - ), - ) - config.hive_base_test = base_test # type: ignore[attr-defined] - - # base_pre=None: seed key funded via CL withdrawal and deterministic - # factory deployed on-chain by ``_session_pre_run``, so genesis only - # needs the fork's required system contracts. - pre_alloc, genesis_header = build_genesis_header(session_fork, None) - genesis_dict = build_client_genesis_dict(pre_alloc, genesis_header) - - client_type = simulator.client_types()[0] - client = base_test.start_client( - client_type=client_type, - environment=build_hive_environment(session_fork, chain_id), - files=build_client_files(genesis_dict), - ) - if client is None: - pytest.exit( - f"Unable to start hive client {client_type.name}. Check the " - "hive server logs for more information." - ) - config.hive_client = client # type: ignore[attr-defined] - logger.info( - f"Started hive client {client_type.name} at {client.ip} " - f"(fork={session_fork.name()}, chain_id={chain_id})" - ) - - config.option.rpc_endpoint = f"http://{client.ip}:8545" - config.option.engine_endpoint = f"http://{client.ip}:8551" - if ( - config.getoption("engine_jwt_secret", default=None) is None - and config.getoption("engine_jwt_secret_file", default=None) is None - ): - config.option.engine_jwt_secret = EngineRPC.DEFAULT_JWT_SECRET - if config.getoption("chain_id", default=None) is None: - config.option.chain_id = chain_id - - @pytest.hookimpl(tryfirst=True) def pytest_configure(config: pytest.Config) -> None: """ @@ -310,7 +184,7 @@ def pytest_configure(config: pytest.Config) -> None: config.hive_base_test = None # type: ignore[attr-defined] config.hive_client = None # type: ignore[attr-defined] if config.getoption("hive_mode", default=False): - _configure_hive(config) + configure_hive(config) else: if not config.getoption("rpc_endpoint", default=None): config.option.rpc_endpoint = "http://localhost:8545" @@ -328,43 +202,7 @@ def pytest_configure(config: pytest.Config) -> None: def pytest_unconfigure(config: pytest.Config) -> None: """Tear down hive resources started in ``pytest_configure``.""" - client = getattr(config, "hive_client", None) - if client is not None: - try: - client.stop() - except Exception as e: - logger.warning(f"Failed to stop hive client: {e}") - config.hive_client = None # type: ignore[attr-defined] - - base_test = getattr(config, "hive_base_test", None) - if base_test is not None: - from hive.testing import HiveTestResult - - test_pass = ( - getattr(config, "_fill_stateful_session_failed", False) is False - ) - try: - base_test.end( - result=HiveTestResult( - test_pass=test_pass, - details=( - "fill-stateful session complete" - if test_pass - else "fill-stateful session had failures" - ), - ) - ) - except Exception as e: - logger.warning(f"Failed to end hive base test: {e}") - config.hive_base_test = None # type: ignore[attr-defined] - - suite = getattr(config, "hive_test_suite", None) - if suite is not None: - try: - suite.end() - except Exception as e: - logger.warning(f"Failed to end hive test suite: {e}") - config.hive_test_suite = None # type: ignore[attr-defined] + teardown_hive(config) def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: @@ -463,12 +301,6 @@ def max_gas_limit_per_test( return fork_at_genesis.transaction_gas_limit_cap() -# Other live-client fixtures (``max_transactions_per_batch``, -# ``default_*``, fee fields, ``dry_run``, ...) come from -# ``shared.live_client_flags``. ``skip_cleanup`` from ``execute.pre_alloc``; -# we force it on in ``pytest_configure``. - - # --------------------------------------------------------------------------- # Backend + session setup # --------------------------------------------------------------------------- diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/hive_session.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/hive_session.py new file mode 100644 index 00000000000..287a2810055 --- /dev/null +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/hive_session.py @@ -0,0 +1,180 @@ +""" +Hive session bootstrap for the fill-stateful plugin. +""" + +import os + +import pytest + +from execution_testing.forks import ( + Fork, + ForkSetAdapter, + InvalidForkError, + TransitionFork, +) +from execution_testing.logging import get_logger +from execution_testing.rpc import EngineRPC +from execution_testing.test_types.chain_config_types import ( + ChainConfigDefaults, +) + +from ..execute.rpc.hive import ( + build_client_files, + build_client_genesis_dict, + build_genesis_header, + build_hive_environment, +) + +logger = get_logger(__name__) + + +def _resolve_session_fork( + config: pytest.Config, +) -> Fork | TransitionFork: + """ + Resolve the single fork from ``--fork`` in hive mode. + """ + fork_arg = config.getoption("single_fork", default="") + if not fork_arg: + pytest.exit( + "--fork is required in --hive-mode (single-fork only).", + returncode=pytest.ExitCode.USAGE_ERROR, + ) + try: + fork_set = ForkSetAdapter.validate_python(fork_arg) + except InvalidForkError: + pytest.exit( + f"Unsupported fork provided to --fork: {fork_arg!r}", + returncode=pytest.ExitCode.USAGE_ERROR, + ) + if len(fork_set) != 1: + pytest.exit( + f"Expected exactly one fork in --hive-mode, got {len(fork_set)} " + f"({sorted(f.name() for f in fork_set)}).", + returncode=pytest.ExitCode.USAGE_ERROR, + ) + return next(iter(fork_set)) + + +def _hive_chain_id(config: pytest.Config) -> int: + """Resolve chain id from CLI/env, defaulting to ChainConfigDefaults.""" + cli_value = config.getoption("chain_id", default=None) + if cli_value is not None: + return int(cli_value) + env_value = os.environ.get("CHAIN_ID") + if env_value is not None: + return int(env_value) + return ChainConfigDefaults.chain_id + + +def configure_hive(config: pytest.Config) -> None: + """ + Start a hive simulator session and a single execution client, then + rewrite ``config.option`` so the rest of the plugin stack (remote.py + in particular) connects to the hive-managed client. + """ + hive_simulator_url = config.getoption("hive_simulator") + if hive_simulator_url is None: + pytest.exit( + "The HIVE_SIMULATOR environment variable is not set.\n\n" + "If running locally, start hive in --dev mode, for example:\n" + "./hive --dev --client go-ethereum\n\n" + "and set the HIVE_SIMULATOR to the reported URL. For example, " + "in bash:\n" + "export HIVE_SIMULATOR=http://127.0.0.1:3000\n" + "or in fish:\n" + "set -x HIVE_SIMULATOR http://127.0.0.1:3000" + ) + from hive.simulation import Simulation + + session_fork = _resolve_session_fork(config) + chain_id = _hive_chain_id(config) + + simulator = Simulation(url=hive_simulator_url) + suite = simulator.start_suite( + name="fill-stateful test suite", + description="Test suite used to drive a fill-stateful session", + ) + config.hive_test_suite = suite # type: ignore[attr-defined] + base_test = suite.start_test( + name="fill-stateful base test", + description=( + "Base test in the fill-stateful suite hosting the long-lived " + "execution client." + ), + ) + config.hive_base_test = base_test # type: ignore[attr-defined] + + # base_pre=None: seed key funded via CL withdrawal and deterministic + # factory deployed on-chain by ``_session_pre_run``, so genesis only + # needs the fork's required system contracts. + pre_alloc, genesis_header = build_genesis_header(session_fork, None) + genesis_dict = build_client_genesis_dict(pre_alloc, genesis_header) + + client_type = simulator.client_types()[0] + client = base_test.start_client( + client_type=client_type, + environment=build_hive_environment(session_fork, chain_id), + files=build_client_files(genesis_dict), + ) + if client is None: + pytest.exit( + f"Unable to start hive client {client_type.name}. Check the " + "hive server logs for more information." + ) + config.hive_client = client # type: ignore[attr-defined] + logger.info( + f"Started hive client {client_type.name} at {client.ip} " + f"(fork={session_fork.name()}, chain_id={chain_id})" + ) + + config.option.rpc_endpoint = f"http://{client.ip}:8545" + config.option.engine_endpoint = f"http://{client.ip}:8551" + if ( + config.getoption("engine_jwt_secret", default=None) is None + and config.getoption("engine_jwt_secret_file", default=None) is None + ): + config.option.engine_jwt_secret = EngineRPC.DEFAULT_JWT_SECRET + if config.getoption("chain_id", default=None) is None: + config.option.chain_id = chain_id + + +def teardown_hive(config: pytest.Config) -> None: + """Tear down hive resources started in ``configure_hive``.""" + client = getattr(config, "hive_client", None) + if client is not None: + try: + client.stop() + except Exception as e: + logger.warning(f"Failed to stop hive client: {e}") + config.hive_client = None # type: ignore[attr-defined] + + base_test = getattr(config, "hive_base_test", None) + if base_test is not None: + from hive.testing import HiveTestResult + + test_pass = ( + getattr(config, "_fill_stateful_session_failed", False) is False + ) + try: + base_test.end( + result=HiveTestResult( + test_pass=test_pass, + details=( + "fill-stateful session complete" + if test_pass + else "fill-stateful session had failures" + ), + ) + ) + except Exception as e: + logger.warning(f"Failed to end hive base test: {e}") + config.hive_base_test = None # type: ignore[attr-defined] + + suite = getattr(config, "hive_test_suite", None) + if suite is not None: + try: + suite.end() + except Exception as e: + logger.warning(f"Failed to end hive test suite: {e}") + config.hive_test_suite = None # type: ignore[attr-defined] From 1d16d66843a6d2f3a5dfcdea48b6d75de91a34e3 Mon Sep 17 00:00:00 2001 From: LouisTsai-Csie Date: Fri, 29 May 2026 09:01:31 +0000 Subject: [PATCH 6/6] feat: inject stub account into hive genesis --- .../plugins/execute/rpc/hive.py | 9 + .../plugins/fill_stateful/fill_stateful.py | 67 +++-- .../plugins/fill_stateful/hive_session.py | 65 ++++- .../fill_stateful/stub_account/__init__.py | 15 ++ .../fill_stateful/stub_account/chainspec.py | 57 ++++ .../stub_account/chainspecs/jochemnet.json | 72 ++++++ .../chainspecs/perf-devnet-3.json | 17 ++ .../fill_stateful/stub_account/template.py | 244 ++++++++++++++++++ .../pytest_commands/plugins/shared/profile.py | 48 ++++ 9 files changed, 567 insertions(+), 27 deletions(-) create mode 100644 packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/__init__.py create mode 100644 packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspec.py create mode 100644 packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspecs/jochemnet.json create mode 100644 packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspecs/perf-devnet-3.json create mode 100644 packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/template.py create mode 100644 packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/profile.py diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/hive.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/hive.py index 6779a0eb903..f7acdd4fb32 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/hive.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/hive.py @@ -2,6 +2,7 @@ import io import json +import time from dataclasses import asdict, replace from pathlib import Path from random import randint @@ -13,6 +14,8 @@ from hive.simulation import Simulation from hive.testing import HiveTest, HiveTestResult, HiveTestSuite +from ...shared import profile as _profile + from execution_testing.base_types import ( Account, EmptyOmmersRoot, @@ -150,7 +153,13 @@ def build_genesis_header( pre_alloc = Alloc.merge(pre_alloc, base_pre) if empty_accounts := pre_alloc.empty_accounts(): raise Exception(f"Empty accounts in pre state: {empty_accounts}") + _state_root_t0 = time.perf_counter() state_root = pre_alloc.state_root() + _profile.write( + "Alloc.state_root", + accounts=len(pre_alloc.root), + elapsed_s=f"{time.perf_counter() - _state_root_t0:.3f}", + ) genesis = FixtureHeader( parent_hash=0, ommers_hash=EmptyOmmersRoot, diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py index 697018a5a2d..12f42e6b122 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/fill_stateful.py @@ -15,6 +15,7 @@ import os import secrets +import time from pathlib import Path from typing import Any, Generator, List, Tuple from urllib.parse import urlparse, urlunparse @@ -48,9 +49,11 @@ from ..execute import contracts from ..execute.rpc.chain_builder_eth_rpc import ChainBuilderEthRPC +from ..shared import profile from ..shared.helpers import is_help_or_collectonly_mode from ..shared.live_client_flags import FEE_BUMP_MULTIPLIER from .hive_session import configure_hive, teardown_hive +from .stub_account import DEFAULT_CHAINSPEC_PATH # 1B ETH for seed account SEED_FUNDING_WEI = 10**9 * 10**18 @@ -103,6 +106,18 @@ def pytest_addoption(parser: pytest.Parser) -> None: "variable." ), ) + group.addoption( + "--chainspec", + action="store", + dest="chainspec", + default=str(DEFAULT_CHAINSPEC_PATH), + type=str, + help=( + "Path to a chainspec JSON file for --hive-mode; its stub " + "accounts are merged into the genesis pre-state. Defaults to " + f"the bundled {DEFAULT_CHAINSPEC_PATH.name}." + ), + ) group.addoption( "--rpc-seed-key", action="store", @@ -118,12 +133,13 @@ def pytest_addoption(parser: pytest.Parser) -> None: "--gas-bump-blocks", action="store", dest="gas_bump_blocks", - default=5000, + default=0, type=int, help=( - "Empty blocks at start to increase block gas limit." - "Each block increase ≤ parent_gas_limit/1024." - "Default: 5000 blocks." + "Empty blocks at start to increase block gas limit " + "(parent_gas_limit/1024 per block). Default: 0 — hive-mode " + "bakes a 1 G genesis gas limit so no ramp is needed. Raise " + "this only if your tests need more than 1 G per block." ), ) group.addoption( @@ -212,10 +228,21 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: session.config._fill_stateful_session_failed = True # type: ignore[attr-defined] -def pytest_runtest_call(item: pytest.Item) -> None: - """Fail in the call phase (→ FAILED, not ERROR) on ``missing_stubs``.""" +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_call(item: pytest.Item) -> Generator[None, None, None]: + """Fail on ``missing_stubs``; time each parametrization's call phase.""" for marker in item.iter_markers("missing_stubs"): pytest.fail(marker.args[0], pytrace=False) + profile.write("test starting", nodeid=item.nodeid) + t0 = time.perf_counter() + outcome = yield + elapsed = time.perf_counter() - t0 + profile.write( + "test done", + nodeid=item.nodeid, + elapsed_s=f"{elapsed:.3f}", + outcome="PASS" if outcome.excinfo is None else "FAIL", + ) # --------------------------------------------------------------------------- @@ -444,9 +471,11 @@ def _session_pre_run( captured: List[EnginePayloadMetadata] = [] # Ramp gas limit (empty blocks) via empty blocks. - bump_payloads = eth_rpc.bump_block_gas_limit( - request.config.getoption("gas_bump_blocks") - ) + gas_bump_blocks = request.config.getoption("gas_bump_blocks") + with profile.phase( + "empty-block gas-limit ramp", requested_blocks=gas_bump_blocks + ): + bump_payloads = eth_rpc.bump_block_gas_limit(gas_bump_blocks) captured.extend(bump_payloads) if bump_payloads: logger.info( @@ -462,7 +491,10 @@ def _session_pre_run( (Address(EOA(key=SENDER_BASE_KEY + i)), POOL_FUNDING_WEI) for i in range(pool_size) ] - fund_payload = eth_rpc.fund_via_withdrawals(funding_targets) + with profile.phase( + "fund_via_withdrawals", recipients=len(funding_targets) + ): + fund_payload = eth_rpc.fund_via_withdrawals(funding_targets) if fund_payload is not None: captured.append(fund_payload) logger.info(f"Funded seed key and {pool_size} sender pool accounts") @@ -476,14 +508,15 @@ def _session_pre_run( ) is None ): - _, deploy_payloads = ( - contracts.deploy_deterministic_factory_contract( - eth_rpc=eth_rpc, - seed_key=session_worker_key, - gas_price=sender_funding_transactions_gas_price, - tx_index=0, + with profile.phase("deploy_deterministic_factory"): + _, deploy_payloads = ( + contracts.deploy_deterministic_factory_contract( + eth_rpc=eth_rpc, + seed_key=session_worker_key, + gas_price=sender_funding_transactions_gas_price, + tx_index=0, + ) ) - ) captured.extend(deploy_payloads) # 5. Capture start block. diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/hive_session.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/hive_session.py index 287a2810055..837f49c852c 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/hive_session.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/hive_session.py @@ -2,7 +2,9 @@ Hive session bootstrap for the fill-stateful plugin. """ +import json import os +import time import pytest @@ -14,6 +16,7 @@ ) from execution_testing.logging import get_logger from execution_testing.rpc import EngineRPC +from execution_testing.test_types.block_types import EnvironmentDefaults from execution_testing.test_types.chain_config_types import ( ChainConfigDefaults, ) @@ -24,9 +27,15 @@ build_genesis_header, build_hive_environment, ) +from ..shared import profile +from .stub_account import get_chainspec logger = get_logger(__name__) +# Block gas limit baked into the hive-mode genesis header. 1 giga-gas is +# enough for the current benchmark suite without an empty-block ramp. +HIVE_GENESIS_GAS_LIMIT = 1_000_000_000 + def _resolve_session_fork( config: pytest.Config, @@ -105,18 +114,50 @@ def configure_hive(config: pytest.Config) -> None: ) config.hive_base_test = base_test # type: ignore[attr-defined] - # base_pre=None: seed key funded via CL withdrawal and deterministic - # factory deployed on-chain by ``_session_pre_run``, so genesis only - # needs the fork's required system contracts. - pre_alloc, genesis_header = build_genesis_header(session_fork, None) - genesis_dict = build_client_genesis_dict(pre_alloc, genesis_header) + profile.start_session() + profile_t0 = time.perf_counter() + + # Bake a fixed 1 G gas limit into the hive genesis header so no + # post-genesis empty-block ramp is needed for normal benchmark sizes. + # Override only by editing here; the previous --genesis-gas-limit + # CLI knob was removed once 1 G proved sufficient end-to-end. + EnvironmentDefaults.gas_limit = HIVE_GENESIS_GAS_LIMIT + profile.write( + "genesis_gas_limit", + wei=HIVE_GENESIS_GAS_LIMIT, + giga_gas=f"{HIVE_GENESIS_GAS_LIMIT / 1_000_000_000:.2f}", + ) - client_type = simulator.client_types()[0] - client = base_test.start_client( - client_type=client_type, - environment=build_hive_environment(session_fork, chain_id), - files=build_client_files(genesis_dict), + with profile.phase("chainspec load + Alloc build"): + base_pre = get_chainspec(config.getoption("chainspec")) + profile.write( + "chainspec accounts (pre-fork merge)", + count=len(base_pre.root), + ) + + with profile.phase("build_genesis_header (incl. state_root)"): + pre_alloc, genesis_header = build_genesis_header( + session_fork, base_pre + ) + + with profile.phase("build_client_genesis_dict (JSON serialise)"): + genesis_dict = build_client_genesis_dict(pre_alloc, genesis_header) + profile.write( + "genesis JSON", + size_kib=f"{len(json.dumps(genesis_dict)) / 1024:.1f}", + alloc_entries=len(genesis_dict.get("alloc", {})), ) + + client_type = simulator.client_types()[0] + with profile.phase( + "hive start_client (container boot + client genesis import)", + client=client_type.name, + ): + client = base_test.start_client( + client_type=client_type, + environment=build_hive_environment(session_fork, chain_id), + files=build_client_files(genesis_dict), + ) if client is None: pytest.exit( f"Unable to start hive client {client_type.name}. Check the " @@ -127,6 +168,10 @@ def configure_hive(config: pytest.Config) -> None: f"Started hive client {client_type.name} at {client.ip} " f"(fork={session_fork.name()}, chain_id={chain_id})" ) + profile.write( + "configure_hive total", + elapsed_s=f"{time.perf_counter() - profile_t0:.3f}", + ) config.option.rpc_endpoint = f"http://{client.ip}:8545" config.option.engine_endpoint = f"http://{client.ip}:8551" diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/__init__.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/__init__.py new file mode 100644 index 00000000000..652bce63d67 --- /dev/null +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/__init__.py @@ -0,0 +1,15 @@ +"""Stub-account generators and chainspec loader for fill-stateful.""" + +from .chainspec import ( + DEFAULT_CHAINSPEC_PATH, + ChainSpec, + get_chainspec, + load_chainspec, +) + +__all__ = [ + "ChainSpec", + "DEFAULT_CHAINSPEC_PATH", + "get_chainspec", + "load_chainspec", +] diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspec.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspec.py new file mode 100644 index 00000000000..23d717b2eb0 --- /dev/null +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspec.py @@ -0,0 +1,57 @@ +""" +Genesis chainspecs for fill-stateful hive mode. + +A :class:`ChainSpec` is a validated list of :mod:`.template` stub-account +models loaded from a user-supplied JSON file. :meth:`ChainSpec.alloc` +expands and merges them into an ``Alloc`` that +``hive_session.configure_hive`` lays on top of the fork's required system +contracts. The ``--chainspec`` CLI flag accepts a path to such a file. + +Example chainspecs ship under ``chainspecs/`` next to this module +(``jochemnet.json``, ``perf-devnet-3.json``); copy and edit them as a +starting point. + +Chainspecs are intentionally small (local-debug genesis). Large +bloatnet-scale state belongs in a state-actor snapshot, not these +Python-built allocs. +""" + +from pathlib import Path +from typing import List, Union + +from pydantic import BaseModel + +from execution_testing.test_types import Alloc + +from .template import AnyStubAccount, StubAlloc + +DEFAULT_CHAINSPEC_PATH: Path = ( + Path(__file__).parent / "chainspecs" / "jochemnet.json" +) + + +class ChainSpec(BaseModel): + """A named genesis pre-state built from stub-account templates.""" + + name: str + stubs: List[AnyStubAccount] + + def alloc(self) -> Alloc: + """Expand and merge every stub into a genesis ``Alloc``.""" + merged: StubAlloc = {} + for stub in self.stubs: + merged.update(stub.expand()) + return Alloc.model_validate(merged) + + +def load_chainspec(path: Union[str, Path]) -> ChainSpec: + """Parse the chainspec JSON file at ``path``.""" + chainspec_path = Path(path) + if not chainspec_path.is_file(): + raise FileNotFoundError(f"chainspec file not found: {chainspec_path}") + return ChainSpec.model_validate_json(chainspec_path.read_text()) + + +def get_chainspec(path: Union[str, Path]) -> Alloc: + """Return the genesis ``Alloc`` for the chainspec JSON at ``path``.""" + return load_chainspec(path).alloc() diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspecs/jochemnet.json b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspecs/jochemnet.json new file mode 100644 index 00000000000..ca47abf3b0e --- /dev/null +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspecs/jochemnet.json @@ -0,0 +1,72 @@ +{ + "name": "jochemnet", + "stubs": [ + { "template": "create2_factory" }, + { + "template": "funded_eoa", + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "balance": "0x33b2e3c91efc989409c0000" + }, + { + "template": "sequential_balance_only_account", + "anchor": "0x1000", + "count": 15000, + "balance": "0x1" + }, + { + "template": "create2_deploys", + "deployer": "0x4e59b44847b379578588920cA78FbF26c0B4956C", + "initcode": "0x7f5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b6000526020600060205e6040600060405e6080600060805e61010060006101005e61020060006102005e61040060006104005e61080060006108005e61100060006110005e61200060006120005e61400060006140005e7f615fff565b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b6000527f5b5b5b5b5b5b5b5b5b5b5b5b000000000000000000000000000000000000000030176020526160006000f3", + "code": "0x6100185600000000000000000000000000000000000000005b", + "count": 15000, + "start": 0 + }, + { + "template": "create_preimage_deploys", + "deployer": "0xA3C1E324CA1CE40DB73ED6026C4A177F099B5770", + "code": "0x606060405236156100495763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416636ea056a98114610052578063c0ee0b8a14610092575b6100505b5b565b005b341561005a57fe5b61007e73ffffffffffffffffffffffffffffffffffffffff60043516602435610104565b604080519115158252519081900360200190f35b341561009a57fe5b604080516020600460443581810135601f810184900484028501840190955284845261005094823573ffffffffffffffffffffffffffffffffffffffff169460248035956064949293919092019181908401838280828437509496506101ef95505050505050565b005b6000805460408051602090810184905281517f3c18d31800000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff878116600483015292519290931692633c18d318926024808301939282900301818787803b151561017b57fe5b6102c65a03f1151561018957fe5b5050506040518051905073ffffffffffffffffffffffffffffffffffffffff1660003660006040516020015260405180838380828437820191505092505050602060405180830381856102c65a03f415156101e057fe5b50506040515190505b92915050565b5b5050505600a165627a7a723058204cdd69fdcf3cf6cbee9677fe380fa5f044048aa9e060ec5619a21ca5a5bd4cd10029", + "count": 15000, + "start": 2 + }, + { + "template": "erc20", + "address": "0x398324972FcE0e89E048c2104f1298031d1931fc", + "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c806340c10f1911610088578063a9059cbb11610063578063a9059cbb1461023e578063b330b8e91461027e578063c1926de514610286578063dd62ed3e14610299575f5ffd5b806340c10f19146101c457806370a08231146101fb57806395d89b411461021a575f5ffd5b806305b3b2b1146100cf57806306fdde03146100eb578063095ea7b31461012157806318160ddd1461014457806323b872dd1461014c578063313ce567146101aa575b5f5ffd5b6100d860015481565b6040519081526020015b60405180910390f35b6101146040518060400160405280600a815260200169213637b0ba2a37b5b2b760b11b81525081565b6040516100e291906103a9565b61013461012f3660046103f9565b6102c3565b60405190151581526020016100e2565b6100d85f5481565b61013461015a366004610421565b6001600160a01b039283165f818152600360209081526040808320338452825280832080548690039055928252600290528181208054849003905592909316825291902080549091019055600190565b6101b2601281565b60405160ff90911681526020016100e2565b6101f96101d23660046103f9565b5f8054820181556001600160a01b0390921682526002602052604090912080549091019055565b005b6100d861020936600461045b565b60026020525f908152604090205481565b61011460405180604001604052806005815260200164109313d05560da1b81525081565b61013461024c3660046103f9565b335f90815260026020526040808220805484900390556001600160a01b03841682529020805482019055600192915050565b6001546100d8565b6101f961029436600461047b565b6102f1565b6100d86102a736600461049b565b600360209081525f928352604080842090915290825290205481565b335f9081526003602090815260408083206001600160a01b0386168452909152902081905560015b92915050565b5f6102fc82846104e0565b9050825b8181101561035257335f818152600260209081526040808320805486900390556001600160a01b0385168084528184208054870190559383526003825280832093835292905220819055600101610300565b5060018190557f81da33eb4626fe0493d92fb5d273452c79e4c61c8defa0504d1deb856bfba73783826103868560026104f3565b6040805193845260208401929092529082015260600160405180910390a1505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b03811681146103f4575f5ffd5b919050565b5f5f6040838503121561040a575f5ffd5b610413836103de565b946020939093013593505050565b5f5f5f60608486031215610433575f5ffd5b61043c846103de565b925061044a602085016103de565b929592945050506040919091013590565b5f6020828403121561046b575f5ffd5b610474826103de565b9392505050565b5f5f6040838503121561048c575f5ffd5b50508035926020909101359150565b5f5f604083850312156104ac575f5ffd5b6104b5836103de565b91506104c3602084016103de565b90509250929050565b634e487b7160e01b5f52601160045260245ffd5b808201808211156102eb576102eb6104cc565b80820281158282048414176102eb576102eb6104cc56fea264697066735822122021beeb2282ee4516ca1f8a0ce2351c6e7f4fcaa42407b632fac5e0730e55654564736f6c634300081e0033" + }, + { + "template": "erc20", + "address": "0x19fc17d87D946BBA47ca276f7b06Ee5737c4679C", + "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c806340c10f1911610088578063a9059cbb11610063578063a9059cbb1461023e578063b330b8e91461027e578063c1926de514610286578063dd62ed3e14610299575f5ffd5b806340c10f19146101c457806370a08231146101fb57806395d89b411461021a575f5ffd5b806305b3b2b1146100cf57806306fdde03146100eb578063095ea7b31461012157806318160ddd1461014457806323b872dd1461014c578063313ce567146101aa575b5f5ffd5b6100d860015481565b6040519081526020015b60405180910390f35b6101146040518060400160405280600a815260200169213637b0ba2a37b5b2b760b11b81525081565b6040516100e291906103a9565b61013461012f3660046103f9565b6102c3565b60405190151581526020016100e2565b6100d85f5481565b61013461015a366004610421565b6001600160a01b039283165f818152600360209081526040808320338452825280832080548690039055928252600290528181208054849003905592909316825291902080549091019055600190565b6101b2601281565b60405160ff90911681526020016100e2565b6101f96101d23660046103f9565b5f8054820181556001600160a01b0390921682526002602052604090912080549091019055565b005b6100d861020936600461045b565b60026020525f908152604090205481565b61011460405180604001604052806005815260200164109313d05560da1b81525081565b61013461024c3660046103f9565b335f90815260026020526040808220805484900390556001600160a01b03841682529020805482019055600192915050565b6001546100d8565b6101f961029436600461047b565b6102f1565b6100d86102a736600461049b565b600360209081525f928352604080842090915290825290205481565b335f9081526003602090815260408083206001600160a01b0386168452909152902081905560015b92915050565b5f6102fc82846104e0565b9050825b8181101561035257335f818152600260209081526040808320805486900390556001600160a01b0385168084528184208054870190559383526003825280832093835292905220819055600101610300565b5060018190557f81da33eb4626fe0493d92fb5d273452c79e4c61c8defa0504d1deb856bfba73783826103868560026104f3565b6040805193845260208401929092529082015260600160405180910390a1505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b03811681146103f4575f5ffd5b919050565b5f5f6040838503121561040a575f5ffd5b610413836103de565b946020939093013593505050565b5f5f5f60608486031215610433575f5ffd5b61043c846103de565b925061044a602085016103de565b929592945050506040919091013590565b5f6020828403121561046b575f5ffd5b610474826103de565b9392505050565b5f5f6040838503121561048c575f5ffd5b50508035926020909101359150565b5f5f604083850312156104ac575f5ffd5b6104b5836103de565b91506104c3602084016103de565b90509250929050565b634e487b7160e01b5f52601160045260245ffd5b808201808211156102eb576102eb6104cc565b80820281158282048414176102eb576102eb6104cc56fea264697066735822122021beeb2282ee4516ca1f8a0ce2351c6e7f4fcaa42407b632fac5e0730e55654564736f6c634300081e0033" + }, + { + "template": "erc20", + "address": "0xf7EfF64A1b7f3dB550A05fF2635Bb9744B8E21eb", + "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c806340c10f1911610088578063a9059cbb11610063578063a9059cbb1461023e578063b330b8e91461027e578063c1926de514610286578063dd62ed3e14610299575f5ffd5b806340c10f19146101c457806370a08231146101fb57806395d89b411461021a575f5ffd5b806305b3b2b1146100cf57806306fdde03146100eb578063095ea7b31461012157806318160ddd1461014457806323b872dd1461014c578063313ce567146101aa575b5f5ffd5b6100d860015481565b6040519081526020015b60405180910390f35b6101146040518060400160405280600a815260200169213637b0ba2a37b5b2b760b11b81525081565b6040516100e291906103a9565b61013461012f3660046103f9565b6102c3565b60405190151581526020016100e2565b6100d85f5481565b61013461015a366004610421565b6001600160a01b039283165f818152600360209081526040808320338452825280832080548690039055928252600290528181208054849003905592909316825291902080549091019055600190565b6101b2601281565b60405160ff90911681526020016100e2565b6101f96101d23660046103f9565b5f8054820181556001600160a01b0390921682526002602052604090912080549091019055565b005b6100d861020936600461045b565b60026020525f908152604090205481565b61011460405180604001604052806005815260200164109313d05560da1b81525081565b61013461024c3660046103f9565b335f90815260026020526040808220805484900390556001600160a01b03841682529020805482019055600192915050565b6001546100d8565b6101f961029436600461047b565b6102f1565b6100d86102a736600461049b565b600360209081525f928352604080842090915290825290205481565b335f9081526003602090815260408083206001600160a01b0386168452909152902081905560015b92915050565b5f6102fc82846104e0565b9050825b8181101561035257335f818152600260209081526040808320805486900390556001600160a01b0385168084528184208054870190559383526003825280832093835292905220819055600101610300565b5060018190557f81da33eb4626fe0493d92fb5d273452c79e4c61c8defa0504d1deb856bfba73783826103868560026104f3565b6040805193845260208401929092529082015260600160405180910390a1505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b03811681146103f4575f5ffd5b919050565b5f5f6040838503121561040a575f5ffd5b610413836103de565b946020939093013593505050565b5f5f5f60608486031215610433575f5ffd5b61043c846103de565b925061044a602085016103de565b929592945050506040919091013590565b5f6020828403121561046b575f5ffd5b610474826103de565b9392505050565b5f5f6040838503121561048c575f5ffd5b50508035926020909101359150565b5f5f604083850312156104ac575f5ffd5b6104b5836103de565b91506104c3602084016103de565b90509250929050565b634e487b7160e01b5f52601160045260245ffd5b808201808211156102eb576102eb6104cc565b80820281158282048414176102eb576102eb6104cc56fea264697066735822122021beeb2282ee4516ca1f8a0ce2351c6e7f4fcaa42407b632fac5e0730e55654564736f6c634300081e0033" + }, + { + "template": "erc20", + "address": "0x365cF1A2532919e1adf8650670B9e01e163a441D", + "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c806340c10f1911610088578063a9059cbb11610063578063a9059cbb1461023e578063b330b8e91461027e578063c1926de514610286578063dd62ed3e14610299575f5ffd5b806340c10f19146101c457806370a08231146101fb57806395d89b411461021a575f5ffd5b806305b3b2b1146100cf57806306fdde03146100eb578063095ea7b31461012157806318160ddd1461014457806323b872dd1461014c578063313ce567146101aa575b5f5ffd5b6100d860015481565b6040519081526020015b60405180910390f35b6101146040518060400160405280600a815260200169213637b0ba2a37b5b2b760b11b81525081565b6040516100e291906103a9565b61013461012f3660046103f9565b6102c3565b60405190151581526020016100e2565b6100d85f5481565b61013461015a366004610421565b6001600160a01b039283165f818152600360209081526040808320338452825280832080548690039055928252600290528181208054849003905592909316825291902080549091019055600190565b6101b2601281565b60405160ff90911681526020016100e2565b6101f96101d23660046103f9565b5f8054820181556001600160a01b0390921682526002602052604090912080549091019055565b005b6100d861020936600461045b565b60026020525f908152604090205481565b61011460405180604001604052806005815260200164109313d05560da1b81525081565b61013461024c3660046103f9565b335f90815260026020526040808220805484900390556001600160a01b03841682529020805482019055600192915050565b6001546100d8565b6101f961029436600461047b565b6102f1565b6100d86102a736600461049b565b600360209081525f928352604080842090915290825290205481565b335f9081526003602090815260408083206001600160a01b0386168452909152902081905560015b92915050565b5f6102fc82846104e0565b9050825b8181101561035257335f818152600260209081526040808320805486900390556001600160a01b0385168084528184208054870190559383526003825280832093835292905220819055600101610300565b5060018190557f81da33eb4626fe0493d92fb5d273452c79e4c61c8defa0504d1deb856bfba73783826103868560026104f3565b6040805193845260208401929092529082015260600160405180910390a1505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b03811681146103f4575f5ffd5b919050565b5f5f6040838503121561040a575f5ffd5b610413836103de565b946020939093013593505050565b5f5f5f60608486031215610433575f5ffd5b61043c846103de565b925061044a602085016103de565b929592945050506040919091013590565b5f6020828403121561046b575f5ffd5b610474826103de565b9392505050565b5f5f6040838503121561048c575f5ffd5b50508035926020909101359150565b5f5f604083850312156104ac575f5ffd5b6104b5836103de565b91506104c3602084016103de565b90509250929050565b634e487b7160e01b5f52601160045260245ffd5b808201808211156102eb576102eb6104cc565b80820281158282048414176102eb576102eb6104cc56fea264697066735822122021beeb2282ee4516ca1f8a0ce2351c6e7f4fcaa42407b632fac5e0730e55654564736f6c634300081e0033" + }, + { + "template": "erc20", + "address": "0xFdC419f77993E0E2E9Fed06299E367F6aEe909bE", + "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c806340c10f1911610088578063a9059cbb11610063578063a9059cbb1461023e578063b330b8e91461027e578063c1926de514610286578063dd62ed3e14610299575f5ffd5b806340c10f19146101c457806370a08231146101fb57806395d89b411461021a575f5ffd5b806305b3b2b1146100cf57806306fdde03146100eb578063095ea7b31461012157806318160ddd1461014457806323b872dd1461014c578063313ce567146101aa575b5f5ffd5b6100d860015481565b6040519081526020015b60405180910390f35b6101146040518060400160405280600a815260200169213637b0ba2a37b5b2b760b11b81525081565b6040516100e291906103a9565b61013461012f3660046103f9565b6102c3565b60405190151581526020016100e2565b6100d85f5481565b61013461015a366004610421565b6001600160a01b039283165f818152600360209081526040808320338452825280832080548690039055928252600290528181208054849003905592909316825291902080549091019055600190565b6101b2601281565b60405160ff90911681526020016100e2565b6101f96101d23660046103f9565b5f8054820181556001600160a01b0390921682526002602052604090912080549091019055565b005b6100d861020936600461045b565b60026020525f908152604090205481565b61011460405180604001604052806005815260200164109313d05560da1b81525081565b61013461024c3660046103f9565b335f90815260026020526040808220805484900390556001600160a01b03841682529020805482019055600192915050565b6001546100d8565b6101f961029436600461047b565b6102f1565b6100d86102a736600461049b565b600360209081525f928352604080842090915290825290205481565b335f9081526003602090815260408083206001600160a01b0386168452909152902081905560015b92915050565b5f6102fc82846104e0565b9050825b8181101561035257335f818152600260209081526040808320805486900390556001600160a01b0385168084528184208054870190559383526003825280832093835292905220819055600101610300565b5060018190557f81da33eb4626fe0493d92fb5d273452c79e4c61c8defa0504d1deb856bfba73783826103868560026104f3565b6040805193845260208401929092529082015260600160405180910390a1505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b03811681146103f4575f5ffd5b919050565b5f5f6040838503121561040a575f5ffd5b610413836103de565b946020939093013593505050565b5f5f5f60608486031215610433575f5ffd5b61043c846103de565b925061044a602085016103de565b929592945050506040919091013590565b5f6020828403121561046b575f5ffd5b610474826103de565b9392505050565b5f5f6040838503121561048c575f5ffd5b50508035926020909101359150565b5f5f604083850312156104ac575f5ffd5b6104b5836103de565b91506104c3602084016103de565b90509250929050565b634e487b7160e01b5f52601160045260245ffd5b808201808211156102eb576102eb6104cc565b80820281158282048414176102eb576102eb6104cc56fea264697066735822122021beeb2282ee4516ca1f8a0ce2351c6e7f4fcaa42407b632fac5e0730e55654564736f6c634300081e0033" + }, + { + "template": "storage_pattern", + "address": "0x3f8074692982594c1936bd27433a8b6e5d77e0f0", + "final": 140000 + }, + { + "template": "storage_pattern", + "address": "0x87a6314da5ac8832f6e7a176c8fb133b19f5be04", + "final": 140000 + }, + { + "template": "storage_pattern", + "address": "0x772604ee92ebc9afa5b6ce561f6f6a4c4cdd214a", + "final": 140000 + } + ] +} diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspecs/perf-devnet-3.json b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspecs/perf-devnet-3.json new file mode 100644 index 00000000000..34bae76e9e7 --- /dev/null +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/chainspecs/perf-devnet-3.json @@ -0,0 +1,17 @@ +{ + "name": "perf-devnet-3", + "stubs": [ + { "template": "create2_factory" }, + { + "template": "funded_eoa", + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "balance": "0x33b2e3c91efc989409c0000" + }, + { + "template": "sequential_balance_only_account", + "anchor": "0x1000", + "count": 15000, + "balance": "0x1" + } + ] +} diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/template.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/template.py new file mode 100644 index 00000000000..f7a9a76d05e --- /dev/null +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/fill_stateful/stub_account/template.py @@ -0,0 +1,244 @@ +""" +Pydantic stub-account templates for the fill-stateful hive genesis. + +Each template is a validated model carrying a ``template`` discriminator +and a polymorphic :meth:`expand` that yields the ``{Address: Account}`` it +contributes to the genesis pre-state. New patterns subclass +:class:`StubAccount` and join :data:`AnyStubAccount`. These mirror the +account/contract templates in ethereum/state-actor; keep parameters modest +— this genesis' state root is built by a pure-Python trie, so bloatnet- +scale state belongs in a state-actor snapshot instead. +""" + +from abc import abstractmethod +from typing import Annotated, Dict, Literal, Optional, Union + +from pydantic import BaseModel, Field, model_validator + +from execution_testing.base_types import ( + Account, + Address, + Bytes, + HexNumber, +) +from execution_testing.test_types import ( + DETERMINISTIC_FACTORY_ADDRESS, + DETERMINISTIC_FACTORY_BYTECODE, + compute_create2_address, + compute_create_address, +) + +StubAlloc = Dict[Address, Account] + + +def _dense_storage(final: int) -> Dict[int, int]: + """Build the dense counter layout: slot 0 = final+1, slot k = k.""" + storage: Dict[int, int] = {0: final + 1} + storage.update({k: k for k in range(1, final + 1)}) + return storage + + +class StoragePatternSpec(BaseModel): + """Reusable dense-storage descriptor (slot 0 = ``final + 1``).""" + + final: int = Field(ge=0) + + +class StubAccount(BaseModel): + """Base class for a parameterized genesis stub-account template.""" + + @abstractmethod + def expand(self) -> StubAlloc: + """Return the ``{address: account}`` entries this template adds.""" + + +class FundedEOA(StubAccount): + """A single plain EOA funded with ``balance``.""" + + template: Literal["funded_eoa"] = "funded_eoa" + address: Address + balance: HexNumber + + def expand(self) -> StubAlloc: + """Fund ``address`` with ``balance``.""" + return {self.address: Account(balance=self.balance)} + + +class Create2Factory(StubAccount): + """The canonical Arachnid CREATE2 deterministic-deployment proxy.""" + + template: Literal["create2_factory"] = "create2_factory" + + def expand(self) -> StubAlloc: + """Pre-deploy the factory at its canonical address.""" + return { + DETERMINISTIC_FACTORY_ADDRESS: Account( + nonce=1, code=DETERMINISTIC_FACTORY_BYTECODE + ) + } + + +class SequentialBalanceOnlyAccount(StubAccount): + """ + ``count`` balance-only accounts at ``anchor + i``, each funded with + ``balance`` (nonce 0, no code). + """ + + template: Literal["sequential_balance_only_account"] = ( + "sequential_balance_only_account" + ) + anchor: HexNumber + count: int = Field(ge=0) + balance: HexNumber + + @model_validator(mode="after") + def _check_range(self) -> "SequentialBalanceOnlyAccount": + """Reject ranges that overflow the 20-byte address space.""" + if self.count and self.anchor + self.count - 1 >= 2**160: + raise ValueError( + "sequential_balance_only_account: address range " + "overflows 20 bytes" + ) + return self + + def expand(self) -> StubAlloc: + """Fund ``count`` accounts starting at ``anchor`` (no code).""" + return { + Address(self.anchor + i): Account(balance=self.balance) + for i in range(self.count) + } + + +class StoragePattern(StubAccount): + """ + Dense counter storage: slot 0 = ``final + 1`` and slot ``k`` = ``k`` + for ``k`` in ``1..final``. No code (stays EIP-7702-delegatable); nonce + is forced to 1 so EIP-161 empty-account pruning can't wipe it. + """ + + template: Literal["storage_pattern"] = "storage_pattern" + address: Address + final: int = Field(ge=0) + balance: HexNumber = HexNumber(0) + + def expand(self) -> StubAlloc: + """Plant the counter storage layout at ``address``.""" + return { + self.address: Account( + nonce=1, + balance=self.balance, + storage=_dense_storage(self.final), + ) + } + + +class Erc20(StubAccount): + """ + Pre-deployed ERC-20 contract at ``address`` with runtime ``code``. + + For bloated-state tests, set ``storage_pattern`` to plant the dense + counter layout at the same address (composes code + storage in one + entry). For ad-hoc state, use ``storage`` directly. + """ + + template: Literal["erc20"] = "erc20" + address: Address + code: Optional[Bytes] = None + storage: Dict[HexNumber, HexNumber] = Field(default_factory=dict) + storage_pattern: Optional[StoragePatternSpec] = None + nonce: int = Field(default=1, ge=1) + balance: HexNumber = HexNumber(0) + + @model_validator(mode="after") + def _require_code(self) -> "Erc20": + """Require runtime code (inline ``code`` is mandatory).""" + if not self.code: + raise ValueError("erc20: 'code' is required (hex runtime bytes)") + return self + + def expand(self) -> StubAlloc: + """Plant the ERC-20 runtime at ``address`` with merged storage.""" + merged: Dict[int, int] = { + int(k): int(v) for k, v in self.storage.items() + } + if self.storage_pattern is not None: + merged.update(_dense_storage(self.storage_pattern.final)) + return { + self.address: Account( + nonce=self.nonce, + balance=self.balance, + code=self.code, + storage=merged, + ) + } + + +class Create2Deploys(StubAccount): + """ + Plant ``code`` at every address derived from + ``CREATE2(deployer, salt, initcode)`` for ``salt`` in + ``[start, start + count)``. The deployer defaults to the canonical + Arachnid factory. Constructors never run — ``code`` may differ from + what ``initcode`` would have returned. + """ + + template: Literal["create2_deploys"] = "create2_deploys" + deployer: Address = DETERMINISTIC_FACTORY_ADDRESS + initcode: Bytes + code: Bytes + count: int = Field(ge=0) + start: int = Field(default=0, ge=0) + + def expand(self) -> StubAlloc: + """Derive ``count`` CREATE2 addresses and plant ``code`` at each.""" + result: StubAlloc = {} + for i in range(self.count): + addr = compute_create2_address( + address=self.deployer, + salt=self.start + i, + initcode=self.initcode, + ) + result[addr] = Account(nonce=1, code=self.code) + return result + + +class CreatePreimageDeploys(StubAccount): + """ + Plant ``code`` at every address derived from + ``CREATE(deployer, nonce)`` for ``nonce`` in + ``[start, start + count)``. The deployer is *not* implicitly funded — + declare a separate ``funded_eoa`` if you need it. + """ + + template: Literal["create_preimage_deploys"] = "create_preimage_deploys" + deployer: Address + code: Bytes + count: int = Field(ge=0) + start: int = Field(default=0, ge=0) + + def expand(self) -> StubAlloc: + """Derive ``count`` CREATE addresses and plant ``code`` at each.""" + result: StubAlloc = {} + for i in range(self.count): + addr = compute_create_address( + address=self.deployer, nonce=self.start + i + ) + result[addr] = Account(nonce=1, code=self.code) + return result + + +# Discriminated union of every concrete template, keyed on ``template``. +# Lets a chainspec hold a heterogeneous, validated (and JSON-parseable) +# list of stub accounts. +AnyStubAccount = Annotated[ + Union[ + FundedEOA, + Create2Factory, + SequentialBalanceOnlyAccount, + StoragePattern, + Erc20, + Create2Deploys, + CreatePreimageDeploys, + ], + Field(discriminator="template"), +] diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/profile.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/profile.py new file mode 100644 index 00000000000..97ecf1b82b4 --- /dev/null +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/profile.py @@ -0,0 +1,48 @@ +""" +Shared profile log for fill-stateful runs. + +Appends one line per timed event to ``PROFILE_FILE`` and stderr. Uses a +plain file so output survives pytest's stdout/stderr capture without +requiring ``-s`` or ``--log-cli-level``. +""" + +import contextlib +import sys +import time +from datetime import datetime +from pathlib import Path +from typing import Iterator + +PROFILE_FILE: Path = Path("/tmp/fill-stateful-profile.log") + + +def start_session() -> None: + """Truncate the profile file and write a session-start marker.""" + PROFILE_FILE.write_text("") + write("session start", at=datetime.now().isoformat(timespec="seconds")) + + +def write(label: str, **fields: object) -> None: + """Append a profile entry to the log file and stderr.""" + parts = [f"[profile] {label}"] + for key, value in fields.items(): + parts.append(f"{key}={value}") + line = " ".join(parts) + print(line, file=sys.stderr, flush=True) + with PROFILE_FILE.open("a") as fh: + fh.write(line + "\n") + + +@contextlib.contextmanager +def phase(label: str, **fields: object) -> Iterator[None]: + """Time a phase. Log start, end, and elapsed seconds.""" + write(f"{label}: starting", **fields) + t0 = time.perf_counter() + try: + yield + finally: + write( + f"{label}: done", + elapsed_s=f"{time.perf_counter() - t0:.3f}", + **fields, + )