diff --git a/docs/filling_tests/fill_stateful.md b/docs/filling_tests/fill_stateful.md index ebbb52bba9..dde7ac8548 100644 --- a/docs/filling_tests/fill_stateful.md +++ b/docs/filling_tests/fill_stateful.md @@ -123,6 +123,7 @@ Optional: - `--output PATH` — default `./fixtures`. - `--clean` — wipe the output dir before filling. - `--extract-opcode-count` — after building each block, trace it via `debug_traceBlockByHash` (a JS opcode-counting tracer) and record per-opcode execution counts (execution-phase blocks only) in the fixture's `_info.metadata.opcode_counts` — an array with one entry per `engineNewPayloads` block (`opcode_counts[i]` is the count for `engineNewPayloads[i]`, or `null` if its trace was unavailable), so multi-block benchmarks keep per-payload granularity. When a benchmark test declares a target opcode count (`fixed_opcode_count`/`expected_opcode_count`), the live-client count is verified against it and the fill fails on >5% divergence. Requires the `debug` namespace with JS tracer support. Adds a full re-execution trace per block, so it is slow and opt-in. +- `--no-reset-between-tests` — skip the between-test rewind to `start_block`, so each test builds on the state the previous one left behind and the client head accumulates upward. Used to pre-populate a datadir (e.g. deploy setup contracts) whose persisted state a later run builds on. The written fixtures are unchanged — each still records its own `start_block` — so a fixture produced this way is only valid to replay against a client already at that `start_block`; do not mix accumulate-state fills and normal single-anchor fills in one output dir. ## Output layout @@ -198,6 +199,7 @@ Both backends satisfy `FillerBackend` (`client_clis/filler_backend.py`). `Client |---|---|---| | `--snapshot-block` | `fill-stateful` | Anchor by 32-byte hash (reorg-safe) or block number; defaults to `latest`. | | `--rpc-seed-key` | `fill-stateful` | Pin the seed EOA; otherwise generated + funded via CL withdrawal. | +| `--no-reset-between-tests` | `fill-stateful` | Skip the between-test rewind so tests accumulate state on the live client (pre-populate a datadir a later run builds on). | | `--default-gas-price`, `--default-max-fee-per-gas`, `--default-max-priority-fee-per-gas`, `--default-max-fee-per-blob-gas` | `shared/live_client_flags` | Pin per-session fees; defaults bump a one-shot live query by `1.5x`. | | `--max-gas-per-test`, `--max-tx-per-batch`, `--transaction-gas-limit`, ... | `shared/live_client_flags` | Generic live-client knobs reused across commands. | 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 c98280b986..83ec19730e 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 @@ -147,6 +147,17 @@ def pytest_addoption(parser: pytest.Parser) -> None: "opt-in." ), ) + group.addoption( + "--no-reset-between-tests", + action="store_true", + dest="no_reset_between_tests", + default=False, + help=( + "Accumulate state across tests (don't rewind between them). Useful" + "for pre-populating datadir. Fixtures remain valid only from their" + "recorded start_block. Don't mix with single-anchor fills." + ), + ) def _resolve_session_fork( @@ -496,6 +507,12 @@ def extract_opcode_count(request: pytest.FixtureRequest) -> bool: return request.config.getoption("extract_opcode_count") +@pytest.fixture(scope="session") +def no_reset_between_tests(request: pytest.FixtureRequest) -> bool: + """Whether --no-reset-between-tests state accumulation is enabled.""" + return request.config.getoption("no_reset_between_tests") + + @pytest.fixture(scope="session") def client_backend( eth_rpc: ChainBuilderEthRPC, @@ -759,6 +776,7 @@ def _reset_chain_between_tests( client_backend: ClientBackend, debug_rpc: DebugRPC, eth_rpc: "ChainBuilderEthRPC", + no_reset_between_tests: bool, ) -> Generator[None, None, None]: """ Rewind to start_block after each test so the chain is identical for @@ -770,6 +788,8 @@ def _reset_chain_between_tests( drifted (e.g. a live reorg). """ yield + if no_reset_between_tests: + return if client_backend.start_block is None: return start_hex = client_backend.start_block["number"] diff --git a/tests/benchmark/helper/account_creator.py b/tests/benchmark/helper/account_creator.py index 2bb72d09cc..fd6fc761ab 100644 --- a/tests/benchmark/helper/account_creator.py +++ b/tests/benchmark/helper/account_creator.py @@ -14,9 +14,9 @@ SequentialAddressLayout, keccak256, ) -from execution_testing.forks import Osaka +from execution_testing.forks import Amsterdam, Osaka -DEFAULT_CODE_SIZE = Osaka.max_code_size() +DEFAULT_CODE_SIZE = Amsterdam.max_code_size() class AccountMode(Enum): @@ -143,7 +143,7 @@ class JochemnetPredeployContractInitcode(ContractInitcode): code_size: int - def __new__(cls, *, code_size: int = DEFAULT_CODE_SIZE) -> Self: + def __new__(cls, *, code_size: int = Osaka.max_code_size()) -> Self: """Assemble the initcode.""" # Each MCOPY doubles the JUMPDEST-filled span (the first copy is # MCOPY(32, 0, 32), since 1 << 5 = 32) until it covers code_size. @@ -312,7 +312,7 @@ def contract_initcode(self) -> ContractInitcode: ) case AccountMode.EXISTING_CONTRACT_JUMPDEST: return JochemnetPredeployContractInitcode( - code_size=self.code_size + code_size=Osaka.max_code_size() ) case _: raise ValueError(f"{self.mode.name} is not a contract") diff --git a/tests/benchmark/stateful/bloatnet/test_setup_contracts.py b/tests/benchmark/stateful/bloatnet/test_setup_contracts.py new file mode 100644 index 0000000000..26bc15d891 --- /dev/null +++ b/tests/benchmark/stateful/bloatnet/test_setup_contracts.py @@ -0,0 +1,165 @@ +"""Deploy the CREATE2 contracts for benchmarks.""" + +import os + +from execution_testing import ( + DETERMINISTIC_FACTORY_ADDRESS, + EOA, + Account, + Alloc, + AuthorizationTuple, + BenchmarkTestFiller, + Fork, + Hash, + Op, + Transaction, + compute_create2_address, +) + +from tests.benchmark.helper.account_creator import ( + AccountCreator, + AccountMode, +) +from tests.benchmark.helper.account_sender_receiver import ( + DELEGATE_BASE_KEY, +) +from tests.benchmark.helper.transactions import ( + pack_transactions_into_blocks, +) +from tests.prague.eip7702_set_code_tx.spec import Spec as Spec7702 + +# Number of CREATE2 receiver contracts deployed per mode. Overridable via +# BLOATNET_RECEIVER_CONTRACT_COUNT so a smoke/plumbing run (e.g. benchmarkoor +# pre_runs) can deploy a small set for fast iteration; defaults to the full +# 100k benchmark set. +RECEIVER_CONTRACT_COUNT = int( + os.environ.get("BLOATNET_RECEIVER_CONTRACT_COUNT", "100000") +) + +CONTRACT_MODES = [ + AccountMode.EXISTING_CONTRACT_MINIMAL, + AccountMode.EXISTING_CONTRACT_SAME_MAX, + AccountMode.EXISTING_CONTRACT_DIFF_MAX, +] + +# Factory-frame and initcode execution costs (CALLDATACOPY, the MCOPY +# doubling loop, memory expansion) plus CREATE2's 63/64 retention. +EXECUTION_GAS_BUFFER = 50_000 + + +def deployment_gas_limit( + fork: Fork, initcode: bytes, runtime_size: int +) -> int: + """ + Return the gas limit for one CREATE2 deployment, derived from the + intrinsic, CREATE2, and code-deposit costs with a margin for the + factory and initcode execution. + """ + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"\xff" * 32 + initcode + ) + create_cost = Op.CREATE2( + value=0, + offset=0, + size=len(initcode), + salt=0, + init_code_size=len(initcode), + ).gas_cost(fork) + deposit_cost = Op.RETURN( + 0, + runtime_size, + code_deposit_size=runtime_size, + ).gas_cost(fork) + base = intrinsic + create_cost + deposit_cost + return base + base // 16 + EXECUTION_GAS_BUFFER + + +def test_deploy_existing_contracts( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, + fork: Fork, + gas_benchmark_value: int, + tx_gas_limit: int, +) -> None: + """ + Deploy the contracts behind the `AccountMode.EXISTING_CONTRACT_*` + receivers via the deterministic CREATE2 factory. + + Delegate deterministic EOAs to EXISTING_CONTRACT_DIFF_MAX receivers. + """ + txs = [] + post: dict = {} + for account_mode in CONTRACT_MODES: + creator = AccountCreator(account_mode) + initcode = creator.initcode + gas_limit = deployment_gas_limit(fork, initcode, creator.runtime_size) + sender = pre.fund_eoa() + for salt in range(RECEIVER_CONTRACT_COUNT): + txs.append( + Transaction( + to=DETERMINISTIC_FACTORY_ADDRESS, + data=Hash(salt) + initcode, + gas_limit=gas_limit, + sender=sender, + ) + ) + # Nonce 1 at the CREATE2-derived address proves deployment. + for salt in (0, RECEIVER_CONTRACT_COUNT - 1): + contract = compute_create2_address( + address=DETERMINISTIC_FACTORY_ADDRESS, + salt=salt, + initcode=initcode, + ) + post[contract] = Account(nonce=1) + + # Delegate authority i to the i-th DIFF receiver (EIP-7702). + delegation_sender = pre.fund_eoa() + intrinsic = fork.transaction_intrinsic_cost_calculator() + # DIFF receivers share one initcode; build it once for CREATE2 derivation. + diff_initcode = AccountCreator( + AccountMode.EXISTING_CONTRACT_DIFF_MAX + ).initcode + + base_gas = intrinsic(authorization_list_or_count=0) + per_auth_gas = intrinsic(authorization_list_or_count=1) - base_gas + gas_buffer = 100_000 + auths_per_tx = max( + 1, (tx_gas_limit - gas_buffer - base_gas) // per_auth_gas + ) + + for start in range(0, RECEIVER_CONTRACT_COUNT, auths_per_tx): + count = min(auths_per_tx, RECEIVER_CONTRACT_COUNT - start) + authorization_list = [] + for i in range(start, start + count): + authority = EOA(key=DELEGATE_BASE_KEY + i) + target = compute_create2_address( + address=DETERMINISTIC_FACTORY_ADDRESS, + salt=i, + initcode=diff_initcode, + ) + authorization_list.append( + AuthorizationTuple(address=target, nonce=0, signer=authority) + ) + if i == 0 or i == RECEIVER_CONTRACT_COUNT - 1: + post[authority] = Account( + nonce=1, + code=Spec7702.delegation_designation(target), + ) + + txs.append( + Transaction( + to=delegation_sender, + gas_limit=( + intrinsic(authorization_list_or_count=count) + gas_buffer + ), + sender=delegation_sender, + authorization_list=authorization_list, + ) + ) + + benchmark_test( + post=post, + blocks=pack_transactions_into_blocks(txs, gas_benchmark_value), + skip_gas_used_validation=True, + expected_receipt_status=1, + )