Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/filling_tests/fill_stateful.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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. |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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"]
Expand Down
8 changes: 4 additions & 4 deletions tests/benchmark/helper/account_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down
165 changes: 165 additions & 0 deletions tests/benchmark/stateful/bloatnet/test_setup_contracts.py
Original file line number Diff line number Diff line change
@@ -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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iiuc (from the PR description) this test function is not really a test, rather it's a helper that is used to deploy contracts, and then the --no-reset-between-tests flag is used to run the actual tests. Is this correct?

If so, I think there has to be a better way to accomplish this, because we are basically overloading the test function to perform other tasks, rather than describing the test pre-requisites inside of the tests that need them.

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,
)
Loading