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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ Users can select any of the artifacts depending on their testing needs for their
- ✨ Added new `Blob` class which can use the ckzg library to generate valid blobs at runtime ([#1614](https://github.com/ethereum/execution-spec-tests/pull/1614)).
- ✨ Added `blob_transaction_test` execute test spec, which allows tests that send blob transactions to a running client and verifying its `engine_getBlobsVX` endpoint behavior ([#1644](https://github.com/ethereum/execution-spec-tests/pull/1644)).
- ✨ Added `execute eth-config` command to test the `eth_config` RPC endpoint of a client, and includes configurations by default for Mainnet, Sepolia, Holesky, and Hoodi ([#1863](https://github.com/ethereum/execution-spec-tests/pull/1863)).
- ✨ Added `--address-stubs` flag to the `execute` command which allows to specify a JSON-formatted string, JSON file or YAML file which contains label-to-address of specific pre-deployed contracts already existing in the network where the tests are executed ([#2073](https://github.com/ethereum/execution-spec-tests/pull/2073)).

### 📋 Misc

Expand Down
78 changes: 78 additions & 0 deletions docs/running_tests/execute/remote.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,84 @@ It is recommended to only run a subset of the tests when executing on a live net
uv run execute remote --fork=Prague --rpc-endpoint=https://rpc.endpoint.io --rpc-seed-key 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f --rpc-chain-id 12345 ./tests/prague/eip7702_set_code_tx/test_set_code_txs.py::test_set_code_to_sstore
```

## Address Stubs for Pre-deployed Contracts

When running tests on networks that already have specific contracts deployed (such as mainnet or testnets with pre-deployed contracts), you can use the `--address-stubs` flag to specify these contracts instead of deploying new ones.

Address stubs allow you to map contract labels used in tests to actual addresses where those contracts are already deployed on the network. This is particularly useful for:

- Testing against mainnet with existing contracts (e.g., Uniswap, Compound)
- Using pre-deployed contracts on testnets
- Testing on bloat-net, a network containing pre-existing contracts with extensive storage history
- Avoiding redeployment of large contracts to save gas and time

### Using Address Stubs

You can provide address stubs in several formats:

**JSON string:**

```bash
uv run execute remote --fork=Prague --rpc-endpoint=https://rpc.endpoint.io --rpc-seed-key 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f --rpc-chain-id 12345 --address-stubs '{"DEPOSIT_CONTRACT": "0x00000000219ab540356cbb839cbe05303d7705fa", "UNISWAP_V3_FACTORY": "0x1F98431c8aD98523631AE4a59f267346ea31F984"}'
```

**JSON file:**

```bash
uv run execute remote --fork=Prague --rpc-endpoint=https://rpc.endpoint.io --rpc-seed-key 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f --rpc-chain-id 12345 --address-stubs ./contracts.json
```

**YAML file:**

```bash
uv run execute remote --fork=Prague --rpc-endpoint=https://rpc.endpoint.io --rpc-seed-key 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f --rpc-chain-id 12345 --address-stubs ./contracts.yaml
```

### Address Stubs File Format

**JSON format (contracts.json):**

```json
{
"DEPOSIT_CONTRACT": "0x00000000219ab540356cbb839cbe05303d7705fa",
"UNISWAP_V3_FACTORY": "0x1F98431c8aD98523631AE4a59f267346ea31F984",
"COMPOUND_COMPTROLLER": "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B"
}
```

**YAML format (contracts.yaml):**

```yaml
DEPOSIT_CONTRACT: 0x00000000219ab540356cbb839cbe05303d7705fa
UNISWAP_V3_FACTORY: 0x1F98431c8aD98523631AE4a59f267346ea31F984
COMPOUND_COMPTROLLER: 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B
```

### How Address Stubs Work

When a test uses a contract label that matches a key in the address stubs, the test framework will:

1. Use the pre-deployed contract at the specified address instead of deploying a new contract
2. Skip the contract deployment transaction, saving gas and time
3. Use the existing contract's code and state for the test

This is particularly useful when testing interactions with well-known contracts that are expensive to deploy or when you want to test against the actual deployed versions of contracts.

### Bloat-net Testing

Address stubs are especially valuable when testing on **bloat-net**, a specialized network that contains pre-existing contracts with extensive storage history. On bloat-net:

- Contracts have been deployed and used extensively, accumulating large amounts of storage data
- The storage state represents real-world usage patterns with complex data structures
- Redeploying these contracts would lose the valuable historical state and storage bloat

Using address stubs on bloat-net allows you to:

- Test against contracts with realistic storage bloat patterns
- Preserve the complex state that has been built up over time
- Avoid the computational and storage costs of recreating this state
- Test edge cases that only emerge with large, real-world storage datasets

## Transaction Metadata on Remote Networks

When executing tests on remote networks, all transactions include metadata that helps with debugging and monitoring. This metadata is embedded in the RPC request ID and includes:
Expand Down
80 changes: 78 additions & 2 deletions src/pytest_plugins/execute/pre_alloc.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
"""Pre-allocation fixtures using for test filling."""

from itertools import count
from pathlib import Path
from random import randint
from typing import Generator, Iterator, List, Literal, Tuple
from typing import Dict, Generator, Iterator, List, Literal, Self, Tuple

import pytest
import yaml
from pydantic import PrivateAttr

from ethereum_test_base_types import Bytes, Number, StorageRootType, ZeroPaddedHexNumber
from ethereum_test_base_types import (
Bytes,
EthereumTestRootModel,
Number,
StorageRootType,
ZeroPaddedHexNumber,
)
from ethereum_test_base_types.conversions import (
BytesConvertible,
FixedSizeBytesConvertible,
Expand Down Expand Up @@ -36,6 +44,51 @@
MAX_INITCODE_SIZE = MAX_BYTECODE_SIZE * 2


class AddressStubs(EthereumTestRootModel[Dict[str, Address]]):
"""
Address stubs class.

The key represents the label that is used in the test to tag the contract, and the value
is the address where the contract is already located at in the current network.
"""

root: Dict[str, Address]

def __contains__(self, item: str) -> bool:
"""Check if an item is in the address stubs."""
return item in self.root

def __getitem__(self, item: str) -> Address:
"""Get an item from the address stubs."""
return self.root[item]

@classmethod
def model_validate_json_or_file(cls, json_data_or_path: str) -> Self:
"""
Try to load from file if the value resembles a path that ends with .json/.yml and the
file exists.
"""
lower_json_data_or_path = json_data_or_path.lower()
if (
lower_json_data_or_path.endswith(".json")
or lower_json_data_or_path.endswith(".yml")
or lower_json_data_or_path.endswith(".yaml")
):
path = Path(json_data_or_path)
if path.is_file():
path_suffix = path.suffix.lower()
if path_suffix == ".json":
return cls.model_validate_json(path.read_text())
elif path_suffix in [".yml", ".yaml"]:
loaded_yaml = yaml.safe_load(path.read_text())
if loaded_yaml is None:
return cls(root={})
return cls.model_validate(loaded_yaml)
if json_data_or_path.strip() == "":
return cls(root={})
return cls.model_validate_json(json_data_or_path)


def pytest_addoption(parser):
"""Add command-line options to pytest."""
pre_alloc_group = parser.getgroup(
Expand Down Expand Up @@ -66,6 +119,15 @@ def pytest_addoption(parser):
type=int,
help="The default amount of wei to fund each EOA in each test with.",
)
pre_alloc_group.addoption(
"--address-stubs",
action="store",
dest="address_stubs",
default=AddressStubs(root={}),
type=AddressStubs.model_validate_json_or_file,
help="The address stubs for contracts that have already been placed in the chain and to "
"use for the test. Can be a JSON formatted string or a path to a YAML or JSON file.",
)


@pytest.hookimpl(trylast=True)
Expand All @@ -80,6 +142,12 @@ def pytest_report_header(config):
return header


@pytest.fixture(scope="session")
def address_stubs(request) -> AddressStubs:
"""Return an address stubs object."""
return request.config.getoption("address_stubs")


@pytest.fixture(scope="session")
def eoa_iterator(request) -> Iterator[EOA]:
"""Return an iterator that generates EOAs."""
Expand All @@ -100,6 +168,7 @@ class Alloc(BaseAlloc):
_evm_code_type: EVMCodeType | None = PrivateAttr(None)
_chain_id: int = PrivateAttr()
_node_id: str = PrivateAttr("")
_address_stubs: AddressStubs = PrivateAttr()

def __init__(
self,
Expand All @@ -112,6 +181,7 @@ def __init__(
eoa_fund_amount_default: int,
evm_code_type: EVMCodeType | None = None,
node_id: str = "",
address_stubs: AddressStubs | None = None,
**kwargs,
):
"""Initialize the pre-alloc with the given parameters."""
Expand All @@ -124,6 +194,7 @@ def __init__(
self._chain_id = chain_id
self._eoa_fund_amount_default = eoa_fund_amount_default
self._node_id = node_id
self._address_stubs = address_stubs or AddressStubs(root={})

def __setitem__(self, address: Address | FixedSizeBytesConvertible, account: Account | None):
"""Set account associated with an address."""
Expand Down Expand Up @@ -161,6 +232,9 @@ def deploy_contract(
if not isinstance(storage, Storage):
storage = Storage(storage) # type: ignore

if label and label in self._address_stubs:
return self._address_stubs[label]

initcode_prefix = Bytecode()

deploy_gas_limit = 21_000 + 32_000
Expand Down Expand Up @@ -435,6 +509,7 @@ def pre(
chain_id: int,
eoa_fund_amount_default: int,
default_gas_price: int,
address_stubs: AddressStubs,
request: pytest.FixtureRequest,
) -> Generator[Alloc, None, None]:
"""Return default pre allocation for all tests (Empty alloc)."""
Expand All @@ -451,6 +526,7 @@ def pre(
chain_id=chain_id,
eoa_fund_amount_default=eoa_fund_amount_default,
node_id=request.node.nodeid,
address_stubs=address_stubs,
)

# Yield the pre-alloc for usage during the test
Expand Down
1 change: 1 addition & 0 deletions src/pytest_plugins/execute/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Unit tests for the execute pytest plugin."""
83 changes: 83 additions & 0 deletions src/pytest_plugins/execute/tests/test_pre_alloc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Test the pre-allocation models used during test execution."""

from typing import Any

import pytest

from ethereum_test_base_types import Address

from ..pre_alloc import AddressStubs


@pytest.mark.parametrize(
"input_value,expected",
[
pytest.param(
"{}",
AddressStubs({}),
id="empty_address_stubs_string",
),
pytest.param(
'{"some_address": "0x0000000000000000000000000000000000000001"}',
AddressStubs({"some_address": Address("0x0000000000000000000000000000000000000001")}),
id="address_stubs_string_with_some_address",
),
],
)
def test_address_stubs(input_value: Any, expected: AddressStubs):
"""Test the address stubs."""
assert AddressStubs.model_validate_json_or_file(input_value) == expected


@pytest.mark.parametrize(
"file_name,file_contents,expected",
[
pytest.param(
"empty.json",
"{}",
AddressStubs({}),
id="empty_address_stubs_json",
),
pytest.param(
"empty.yaml",
"",
AddressStubs({}),
id="empty_address_stubs_yaml",
),
pytest.param(
"one_address.json",
'{"DEPOSIT_CONTRACT_ADDRESS": "0x00000000219ab540356cbb839cbe05303d7705fa"}',
AddressStubs(
{
"DEPOSIT_CONTRACT_ADDRESS": Address(
"0x00000000219ab540356cbb839cbe05303d7705fa"
),
}
),
id="single_address_json",
),
pytest.param(
"one_address.yaml",
"DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cbb839cbe05303d7705fa",
AddressStubs(
{
"DEPOSIT_CONTRACT_ADDRESS": Address(
"0x00000000219ab540356cbb839cbe05303d7705fa"
),
}
),
id="single_address_yaml",
),
],
)
def test_address_stubs_from_files(
pytester: pytest.Pytester,
file_name: str,
file_contents: str,
expected: AddressStubs,
):
"""Test the address stubs."""
filename = pytester.path.joinpath(file_name)
filename.write_text(file_contents)

assert AddressStubs.model_validate_json_or_file(str(filename)) == expected
Loading