Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .claude/commands/write-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ Conventions and patterns for writing consensus tests. Run this skill before writ
- `@pytest.mark.parametrize("name", [pytest.param(val, id="label"), ...])` with descriptive `id=` strings
- Stack parametrize decorators for multiple dimensions

## After Writing Tests

After writing or modifying tests, ask the user: "Would you like me to load the `/fill-tests` skill to verify the new tests fill correctly? (This loads an additional skill into context.)" If they agree, run `/fill-tests`, fill the new tests, then inspect the generated fixture JSON to verify the fixture contents match what the test intends.

## References

See `docs/writing_tests/` and `docs/writing_tests/opcode_metadata.md` for detailed documentation.
18 changes: 0 additions & 18 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,24 +121,6 @@ jobs:
- name: Run json infra tests
run: tox -e json_infra -- --file-list="${{ steps.get-changed-files.outputs.file_list }}"

optimized:
runs-on: [self-hosted-ghr, size-xl-x64]
needs: static
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
with:
submodules: recursive
fetch-depth: 0 # Fetch full history for commit comparison
- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.11"
- uses: ./.github/actions/setup-env
- uses: ./.github/actions/get-changed-files
id: get-changed-files
- name: Run optimized tests
run: tox -e optimized -- --file-list="${{ steps.get-changed-files.outputs.file_list }}"

tests_pytest_py3:
runs-on: [self-hosted-ghr, size-xl-x64]
needs: static
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def deploy_deterministic_factory_contract(
tx_index=tx_index,
)
tx_index += 1
eth_rpc.send_wait_transaction(fund_tx)
eth_rpc.send_wait_transactions([fund_tx])
logger.info(f"Funding transaction mined: {fund_tx.hash}")

# Add deployment transaction.
Expand All @@ -102,7 +102,7 @@ def deploy_deterministic_factory_contract(
tx_index=tx_index,
)
tx_index += 1
eth_rpc.send_wait_transaction(deploy_tx)
eth_rpc.send_wait_transactions([deploy_tx])
logger.info(f"Deployment transaction mined: {deploy_tx.hash}")
deployment_contract_code = eth_rpc.get_code(DETERMINISTIC_FACTORY_ADDRESS)
logger.info(f"Deployment contract code: {deployment_contract_code}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import pytest
from pytest_metadata.plugin import metadata_key

from execution_testing.base_types import Account
from execution_testing.base_types import Alloc as BaseAlloc
from execution_testing.execution import BaseExecute
from execution_testing.forks import Fork
from execution_testing.logging import get_logger
Expand Down Expand Up @@ -625,6 +627,8 @@ def base_test_parametrizer_func(
max_fee_per_blob_gas: int,
max_gas_limit_per_test: int | None,
gas_limit_accumulator: GasInfoAccumulator,
is_tx_gas_heavy_test: bool,
is_exception_test: bool,
) -> Type[BaseTest]:
"""
Fixture used to instantiate an auto-fillable BaseTest object from
Expand All @@ -648,17 +652,21 @@ def base_test_parametrizer_func(
)

class BaseTestWrapper(cls): # type: ignore
__is_base_test_wrapper__ = True

def __init__(self, *args: Any, **kwargs: Any) -> None:
if "pre" not in kwargs:
kwargs["pre"] = pre
elif kwargs["pre"] != pre:
raise ValueError(
"The pre-alloc object was modified by the test."
)
# Set default for expected_benchmark_gas_used
if "expected_benchmark_gas_used" not in kwargs:
kwargs["expected_benchmark_gas_used"] = gas_benchmark_value
kwargs["fork"] = fork
kwargs["operation_mode"] = request.config.op_mode
kwargs["is_tx_gas_heavy_test"] = is_tx_gas_heavy_test
kwargs["is_exception_test"] = is_exception_test
kwargs |= {
p: request.getfixturevalue(p)
for p in cls_fixture_parameters
Expand All @@ -668,7 +676,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
request.node.config.sender_address = str(pre._sender)

super(BaseTestWrapper, self).__init__(*args, **kwargs)
self._request = request
execute = self.execute(execute_format=execute_format)

# get balances of required sender accounts
Expand Down Expand Up @@ -711,31 +718,47 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
# send the funds to the required sender accounts
pre.send_pending_transactions()

for (
deployed_contract,
expected_code,
) in pre._deployed_contracts:
actual_code = eth_rpc.get_code(deployed_contract)
if actual_code != expected_code:
msg = (
f"Deployed test contract didn't match expected "
f"code at address {deployed_contract} "
f"(not enough gas_limit?).\n"
f"Expected: {expected_code}\n"
f"Actual: {actual_code}"
)
logger.error(msg)
raise Exception(msg)
if pre._deployed_contracts:
contract_alloc = BaseAlloc(
root={
addr: Account()
for addr, _ in pre._deployed_contracts
}
)
actual_alloc = eth_rpc.get_alloc(contract_alloc)
for (
deployed_contract,
expected_code,
) in pre._deployed_contracts:
actual_account = actual_alloc.root[deployed_contract]
assert actual_account is not None
actual_code = actual_account.code
if actual_code != expected_code:
msg = (
f"Deployed test contract didn't match "
f"expected code at address "
f"{deployed_contract} "
f"(not enough gas_limit?).\n"
f"Expected: {expected_code}\n"
f"Actual: {actual_code}"
)
logger.error(msg)
raise Exception(msg)
request.node.config.funded_accounts = ", ".join(
[str(eoa) for eoa in pre._funded_eoa]
)

execute.execute(
execute_result = execute.execute(
fork=fork,
eth_rpc=eth_rpc,
engine_rpc=engine_rpc,
request=request,
)
self.validate_benchmark_gas(
benchmark_gas_used=execute_result.benchmark_gas_used,
gas_benchmark_value=gas_benchmark_value,
)

collector.collect(request.node.nodeid, execute)

return BaseTestWrapper
Expand All @@ -744,7 +767,12 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:


# Dynamically generate a pytest fixture for each test spec type.
for cls in BaseTest.spec_types.values():
for name, cls in BaseTest.spec_types.items():
if getattr(cls, "__is_base_test_wrapper__", False):
raise RuntimeError(
f"Test spec type {name}: {cls.__name__} is already wrapped. "
f"{BaseTest.spec_types.items()}."
)
# Fixture needs to be defined in the global scope so pytest can detect it.
globals()[cls.pytest_parameter_name()] = base_test_parametrizer(cls)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,17 +394,8 @@ def _deterministic_deploy_contract(

self._deployed_contracts.append((contract_address, deploy_code))

balance = self._eth_rpc.get_balance(contract_address)
nonce = self._eth_rpc.get_transaction_count(contract_address)
self.__internal_setitem__(
contract_address,
Account(
nonce=nonce,
balance=balance,
code=deploy_code,
storage={},
),
)
account = self._eth_rpc.get_account(contract_address)
self.__internal_setitem__(contract_address, account)

contract_address.label = label
return contract_address
Expand Down Expand Up @@ -446,27 +437,20 @@ def _deploy_contract(
f"Using address stub '{stub}' at {contract_address} "
f"(label={label})"
)
code = self._eth_rpc.get_code(contract_address)
account = self._eth_rpc.get_account(contract_address)
code = account.code
if code == b"":
raise ValueError(
f"Stub {stub} at {contract_address} has no code"
)
balance = self._eth_rpc.get_balance(contract_address)
nonce = self._eth_rpc.get_transaction_count(contract_address)
balance = account.balance
nonce = account.nonce
bal_eth = balance / 10**18
logger.debug(
f"Stub contract {contract_address}: balance={bal_eth:.18f} "
f"ETH, nonce={nonce}, code_size={len(code)} bytes"
)
self.__internal_setitem__(
contract_address,
Account(
nonce=nonce,
balance=balance,
code=code,
storage={},
),
)
self.__internal_setitem__(contract_address, account)
return contract_address

initcode_prefix = Bytecode()
Expand Down Expand Up @@ -827,36 +811,14 @@ def send_pending_transactions(self) -> List[TransactionByHashResponse]:
f"(deployed_contracts={len(self._deployed_contracts)}, "
f"funded_eoas={len(self._funded_eoa)})"
)
transaction_batches: List[List[PendingTransaction]] = []
last_tx_batch: List[PendingTransaction] = []
max_txs_per_batch = 100
for tx in self._pending_txs:
assert tx.value is not None, (
"Transaction value must be set before sending them to the RPC."
)
if len(last_tx_batch) >= max_txs_per_batch:
transaction_batches.append(last_tx_batch)
last_tx_batch = []
last_tx_batch.append(tx)
if last_tx_batch:
transaction_batches.append(last_tx_batch)

responses: List[TransactionByHashResponse] = []
for tx_batch in transaction_batches:
txs = [tx.with_signature_and_sender() for tx in tx_batch]
tx_hashes = self._eth_rpc.send_transactions(txs)
hash_strs = [str(h) for h in tx_hashes[:5]]
n_hashes = len(tx_hashes)
extra = f" and {n_hashes - 5} more" if n_hashes > 5 else ""
logger.info(f"Sent {n_hashes} transactions: {hash_strs}{extra}")
logger.info(
f"Waiting for {len(tx_batch)} transactions to be included "
"in blocks"
)
responses += self._eth_rpc.wait_for_transactions(tx_batch)
logger.info(
f"All {len(responses)} transactions confirmed in blocks"
)

txs = [tx.with_signature_and_sender() for tx in self._pending_txs]
responses = self._eth_rpc.send_wait_transactions(txs)

for response in responses:
logger.debug(f"Transaction response: {response.model_dump_json()}")
return responses
Expand Down Expand Up @@ -928,22 +890,30 @@ def pre(
return

# Refund all EOAs (regardless of whether the test passed or failed)
funded_eoas = pre._funded_eoa
logger.info(
f"Starting cleanup phase: refunding {len(pre._funded_eoa)} funded EOAs"
f"Starting cleanup phase: refunding {len(funded_eoas)} funded EOAs"
)
refund_txs = []

if not funded_eoas:
logger.info("No funded EOAs to refund")
return

# Build refund transactions
refund_txs: List[Transaction] = []
skipped_refunds = 0
error_refunds = 0
for idx, eoa in enumerate(pre._funded_eoa):
remaining_balance = eth_rpc.get_balance(eoa)
eoa.nonce = Number(eth_rpc.get_transaction_count(eoa))
refund_gas_limit = 21_000
tx_cost = refund_gas_limit * max_fee_per_gas
refund_gas_limit = 21_000
tx_cost = refund_gas_limit * max_fee_per_gas
for idx, eoa in enumerate(funded_eoas):
account = eth_rpc.get_account(eoa, skip_code=True)
remaining_balance = account.balance
eoa.nonce = Number(account.nonce)
if remaining_balance < tx_cost:
rem_eth = remaining_balance / 10**18
cost_eth = tx_cost / 10**18
logger.debug(
f"Skipping refund for EOA {eoa} (label={eoa.label}): "
f"Skipping refund for EOA {eoa} "
f"(label={eoa.label}): "
f"insufficient balance {rem_eth:.18f} ETH < "
f"transaction cost {cost_eth:.18f} ETH"
)
Expand All @@ -954,14 +924,15 @@ def pre(
rem_eth = remaining_balance / 10**18
cost_eth = tx_cost / 10**18
logger.debug(
f"Preparing refund transaction for EOA {eoa} (label={eoa.label}): "
f"Preparing refund transaction for EOA {eoa} "
f"(label={eoa.label}): "
f"{ref_eth:.18f} ETH (remaining: {rem_eth:.18f} ETH, "
f"cost: {cost_eth:.18f} ETH)"
)
refund_tx = Transaction(
sender=eoa,
to=worker_key,
gas_limit=21_000,
gas_limit=refund_gas_limit,
max_fee_per_gas=max_fee_per_gas,
max_priority_fee_per_gas=max_priority_fee_per_gas,
value=refund_value,
Expand All @@ -973,35 +944,18 @@ def pre(
target=eoa.label,
tx_index=idx,
)
try:
logger.info(
f"Sending refund transaction for EOA {eoa}: {refund_tx.hash}"
)
refund_tx_hash = eth_rpc.send_transaction(refund_tx)
logger.info(f"Refund transaction sent: {refund_tx_hash}")
refund_txs.append(refund_tx)
except Exception as e:
eoa_key = eoa.key
logger.error(
f"Error sending refund transaction for EOA {eoa}: {e}."
)
if eoa_key is not None:
logger.info(
f"Retrieve funds manually from EOA {eoa} "
f"using private key {eoa_key.hex()}."
)
error_refunds += 1
continue
refund_txs.append(refund_tx)

if refund_txs:
logger.info(
f"Waiting for {len(refund_txs)} refund transactions "
f"({skipped_refunds} skipped due to insufficient balance, "
f"{error_refunds} errored)"
f"Sending {len(refund_txs)} refund transactions "
f"({skipped_refunds} skipped due to insufficient balance)"
)
eth_rpc.wait_for_transactions(refund_txs)
eth_rpc.send_wait_transactions(refund_txs)
logger.info(f"All {len(refund_txs)} refund transactions confirmed")
else:
logger.info(
f"No refund transactions to send ({skipped_refunds} EOAs skipped "
f"due to insufficient balance, {error_refunds} errored)"
f"No refund transactions to send "
f"({skipped_refunds} EOAs skipped "
f"due to insufficient balance)"
)
Loading
Loading