Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugins): Features plug-in, EIP-7702 mode #781

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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 @@ -35,6 +35,7 @@ Test fixtures for use by clients are available for each release on the [Github r
- ✨ Added a new flag `--solc-version` to the `fill` command, which allows the user to specify the version of the Solidity compiler to use when compiling Yul source code; this version will now be automatically downloaded by `fill` via [`solc-select`](https://github.com/crytic/solc-select) ([#772](https://github.com/ethereum/execution-spec-tests/pull/772)).
- 🐞 Fix usage of multiple `@pytest.mark.with_all*` markers which shared parameters, such as `with_all_call_opcodes` and `with_all_create_opcodes` which shared `evm_code_type`, and now only parametrize compatible values ([#762](https://github.com/ethereum/execution-spec-tests/pull/762)).
- ✨ Added `selector` and `marks` fields to all `@pytest.mark.with_all*` markers, which allows passing lambda functions to select or mark specific parametrized values (see [documentation](https://ethereum.github.io/execution-spec-tests/main/writing_tests/test_markers/#covariant-marker-keyword-arguments) for more information) ([#762](https://github.com/ethereum/execution-spec-tests/pull/762)).
- ✨ Added `--feature` flag to the `fill` command, which allows the user to enable features for all tests during filling (features currently supported: `eip_7702`, `eof_v1`) ([#781](https://github.com/ethereum/execution-spec-tests/pull/781)).

### 🔧 EVM Tools

Expand Down
3 changes: 3 additions & 0 deletions docs/library/pytest_plugins/features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Features Plugin

::: pytest_plugins.features
1 change: 1 addition & 0 deletions docs/library/pytest_plugins/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
* [Overview](index.md)
* [Forks](forks.md)
* [Test Filler](filler.md)
* [Features](features.md)
* [Spec Version Checker](spec_version_checker.md)
1 change: 1 addition & 0 deletions docs/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@
* [EVM Transition Tool Package](library/evm_transition_tool.md)
* [Pytest Plugins](library/pytest_plugins/index.md)
* [Filler Plugin](library/pytest_plugins/filler.md)
* [Features Plugin](library/pytest_plugins/features.md)
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ markers =
slow
pre_alloc_modify
addopts =
-p pytest_plugins.features.features
-p pytest_plugins.filler.pre_alloc
-p pytest_plugins.filler.solc
-p pytest_plugins.filler.filler
Expand Down
2 changes: 1 addition & 1 deletion src/cli/tests/test_pytest_fill_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_fill_help(runner):
assert result.exit_code == pytest.ExitCode.OK
assert "[--evm-bin EVM_BIN]" in result.output
assert "[--traces]" in result.output
assert "[--evm-code-type EVM_CODE_TYPE]" in result.output
assert "--feature" in result.output
assert "--help" in result.output
assert "Arguments defining evm executable behavior:" in result.output

Expand Down
7 changes: 7 additions & 0 deletions src/pytest_plugins/features/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
Pytest plugin to enable feature-based modification during test filling/execution.
"""

from .features import Features

__all__ = ["Features"]
84 changes: 84 additions & 0 deletions src/pytest_plugins/features/features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
Enable feature-specific functionality on all tests during filling/execution.
"""

from enum import Flag, auto

import pytest

from ethereum_test_vm import EVMCodeType


class Features(Flag):
Copy link
Member

Choose a reason for hiding this comment

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

Nice! Didn't know Flag. Love the example from the docs: https://docs.python.org/3/library/enum.html#enum.Flag

"""
Enumerates the available features.

Each feature can be passed to the parameter flag `--feature` as a (case-insensitive) string,
and multiple features can be enabled by repeating the flag.
"""

NONE = 0
EIP_7702 = auto()
"""
Enables tests to be filled and executed in EIP-7702 mode, i.e. addresses returned by
`pre.deploy_contract` is not the contract itself but an EOA account that delegates
to the contract.
"""
EOF_V1 = auto()
"""
Enables tests to be filled and executed in EOF V1 mode, i.e. all test contracts deployed during
tests are automatically wrapped in an EOF V1 container.
"""

def __str__(self):
"""
Returns the string representation of the feature.
"""
return self.name


def pytest_addoption(parser: pytest.Parser):
"""
Adds command-line options to pytest.
"""
features_group = parser.getgroup(
"features", "Arguments related to enabling specific testing of features"
)

features_group.addoption(
"--feature",
action="append",
default=[],
help="Enable a feature (may be specified multiple times, once per feature). "
"Supported features: " + ", ".join(str(feature).lower() for feature in Features),
)


@pytest.fixture(scope="session")
def features(request):
"""
Returns the enabled features.
"""
features_str_list = request.config.getoption("feature")
features = Features.NONE
for feature_str in features_str_list:
features |= Features[feature_str.upper()]
return features


@pytest.fixture(scope="session")
def eip_7702_feature(features: Features) -> bool:
"""
Returns whether the EIP-7702 mode is enabled.
"""
return Features.EIP_7702 in features


@pytest.fixture(autouse=True, scope="session")
def evm_code_type(features: Features) -> EVMCodeType | None:
"""
Returns the default EVM code type for all tests.
"""
if Features.EOF_V1 in features:
return EVMCodeType.EOF_V1
return None
82 changes: 47 additions & 35 deletions src/pytest_plugins/filler/pre_alloc.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,10 @@ def pytest_addoption(parser: pytest.Parser):
"""
Adds command-line options to pytest.
"""
pre_alloc_group = parser.getgroup("pre_alloc", "Arguments defining pre-allocation behavior.")

pre_alloc_group.addoption(
"--strict-alloc",
action="store_true",
dest="strict_alloc",
default=False,
help=("[DEBUG ONLY] Disallows deploying a contract in a predefined address."),
pre_alloc_group = parser.getgroup(
"pre_alloc", "Arguments defining pre-allocation behavior during test filling"
)

pre_alloc_group.addoption(
"--ca-start",
"--contract-address-start",
Expand All @@ -67,13 +62,11 @@ def pytest_addoption(parser: pytest.Parser):
help="The address increment value to each deployed contract by a test.",
)
pre_alloc_group.addoption(
"--evm-code-type",
action="store",
dest="evm_code_type",
default=None,
type=EVMCodeType,
choices=list(EVMCodeType),
help="Type of EVM code to deploy in each test by default.",
"--strict-alloc",
action="store_true",
dest="strict_alloc",
default=False,
help=("[DEBUG ONLY] Disallows deploying a contract in a predefined address."),
)


Expand All @@ -86,6 +79,9 @@ class AllocMode(IntEnum):
STRICT = 1


SET_CODE_DELEGATION_DESIGNATION = bytes.fromhex("ef0100")


class Alloc(BaseAlloc):
"""
Allocation of accounts in the state, pre and post test execution.
Expand All @@ -95,6 +91,7 @@ class Alloc(BaseAlloc):
_contract_address_iterator: Iterator[Address] = PrivateAttr(...)
_eoa_iterator: Iterator[EOA] = PrivateAttr(...)
_evm_code_type: EVMCodeType | None = PrivateAttr(None)
_eip_7702_mode: bool = PrivateAttr(False)

def __init__(
self,
Expand All @@ -103,6 +100,7 @@ def __init__(
contract_address_iterator: Iterator[Address],
eoa_iterator: Iterator[EOA],
evm_code_type: EVMCodeType | None = None,
eip_7702_feature: bool = False,
**kwargs,
):
"""
Expand All @@ -113,6 +111,7 @@ def __init__(
self._contract_address_iterator = contract_address_iterator
self._eoa_iterator = eoa_iterator
self._evm_code_type = evm_code_type
self._eip_7702_mode = eip_7702_feature

def __setitem__(self, address: Address | FixedSizeBytesConvertible, account: Account | None):
"""
Expand Down Expand Up @@ -164,15 +163,37 @@ def deploy_contract(
if self._alloc_mode == AllocMode.STRICT:
assert Number(nonce) >= 1, "impossible to deploy contract with nonce lower than one"

super().__setitem__(
contract_address,
Account(
if self._eip_7702_mode:
super().__setitem__(
contract_address,
Account(
nonce=nonce,
balance=0,
code=self.code_pre_processor(code, evm_code_type=evm_code_type),
storage={},
),
)

eoa_set_code_proxy = next(self._eoa_iterator)
account = Account(
nonce=nonce,
balance=balance,
code=self.code_pre_processor(code, evm_code_type=evm_code_type),
code=SET_CODE_DELEGATION_DESIGNATION + bytes(contract_address),
storage=storage,
),
)
)
super().__setitem__(eoa_set_code_proxy, account)
contract_address = eoa_set_code_proxy
else:
super().__setitem__(
contract_address,
Account(
nonce=nonce,
balance=balance,
code=self.code_pre_processor(code, evm_code_type=evm_code_type),
storage=storage,
),
)

if label is None:
# Try to deduce the label from the code
frame = inspect.currentframe()
Expand Down Expand Up @@ -212,7 +233,7 @@ def fund_eoa(
account = Account(
nonce=1,
balance=amount,
storage=storage,
storage=storage if storage is not None else {},
)
eoa.nonce = Number(1)

Expand Down Expand Up @@ -290,31 +311,22 @@ def eoa_iterator() -> Iterator[EOA]:
return iter(eoa_by_index(i).copy() for i in count())


@pytest.fixture(autouse=True)
def evm_code_type(request: pytest.FixtureRequest) -> EVMCodeType:
"""
Returns the default EVM code type for all tests (LEGACY).
"""
parameter_evm_code_type = request.config.getoption("evm_code_type")
if parameter_evm_code_type is not None:
assert type(parameter_evm_code_type) is EVMCodeType, "Invalid EVM code type"
return parameter_evm_code_type
return EVMCodeType.LEGACY


@pytest.fixture(scope="function")
def pre(
alloc_mode: AllocMode,
contract_address_iterator: Iterator[Address],
eoa_iterator: Iterator[EOA],
evm_code_type: EVMCodeType,
evm_code_type: EVMCodeType | None,
eip_7702_feature: bool,
) -> Alloc:
"""
Returns the default pre allocation for all tests (Empty alloc).
"""
# TODO: Compare `eip_7702_feature` against the current fork.
return Alloc(
alloc_mode=alloc_mode,
contract_address_iterator=contract_address_iterator,
eoa_iterator=eoa_iterator,
evm_code_type=evm_code_type,
eip_7702_feature=eip_7702_feature,
)
33 changes: 26 additions & 7 deletions src/pytest_plugins/filler/tests/test_pre_alloc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ..pre_alloc import (
CONTRACT_ADDRESS_INCREMENTS_DEFAULT,
CONTRACT_START_ADDRESS_DEFAULT,
SET_CODE_DELEGATION_DESIGNATION,
Alloc,
AllocMode,
contract_address_iterator,
Expand All @@ -27,23 +28,30 @@
pytest.mark.parametrize("contract_start_address", [CONTRACT_START_ADDRESS_DEFAULT]),
pytest.mark.parametrize("contract_address_increments", [CONTRACT_ADDRESS_INCREMENTS_DEFAULT]),
pytest.mark.parametrize("evm_code_type", [EVMCodeType.LEGACY, EVMCodeType.EOF_V1]),
pytest.mark.parametrize("eip_7702_feature", [False, True]),
]


def test_alloc_deploy_contract(pre: Alloc, evm_code_type: EVMCodeType):
def test_alloc_deploy_contract(pre: Alloc, evm_code_type: EVMCodeType, eip_7702_feature: bool):
"""
Test `Alloc.deploy_contract` functionallity.
"""
contract_1 = pre.deploy_contract(Op.SSTORE(0, 1) + Op.STOP)
contract_2 = pre.deploy_contract(Op.SSTORE(0, 2) + Op.STOP)
assert contract_1 == Address(CONTRACT_START_ADDRESS_DEFAULT)
assert contract_2 == Address(
CONTRACT_START_ADDRESS_DEFAULT + CONTRACT_ADDRESS_INCREMENTS_DEFAULT
)
if eip_7702_feature:
assert contract_1 == TestAddress
assert contract_2 == TestAddress2
else:
assert contract_1 == Address(CONTRACT_START_ADDRESS_DEFAULT)
assert contract_2 == Address(
CONTRACT_START_ADDRESS_DEFAULT + CONTRACT_ADDRESS_INCREMENTS_DEFAULT
)
assert contract_1 in pre
assert contract_2 in pre
pre_contract_1_account = pre[contract_1]
pre_contract_2_account = pre[contract_2]
pre_contract_1_account = pre[Address(CONTRACT_START_ADDRESS_DEFAULT)]
pre_contract_2_account = pre[
Address(CONTRACT_START_ADDRESS_DEFAULT + CONTRACT_ADDRESS_INCREMENTS_DEFAULT)
]
assert pre_contract_1_account is not None
assert pre_contract_2_account is not None
if evm_code_type == EVMCodeType.LEGACY:
Expand All @@ -58,6 +66,17 @@ def test_alloc_deploy_contract(pre: Alloc, evm_code_type: EVMCodeType):
b"\xef\x00\x01\x01\x00\x04\x02\x00\x01\x00\x06\x04\x00\x00\x00\x00\x80\x00"
+ b"\x02`\x02`\x00U\x00"
)
if eip_7702_feature:
eoa_set_code_1 = pre[contract_1]
eoa_set_code_2 = pre[contract_2]
assert eoa_set_code_1 is not None
assert eoa_set_code_2 is not None
assert eoa_set_code_1.code == SET_CODE_DELEGATION_DESIGNATION + Address(
CONTRACT_START_ADDRESS_DEFAULT
)
assert eoa_set_code_2.code == SET_CODE_DELEGATION_DESIGNATION + Address(
CONTRACT_START_ADDRESS_DEFAULT + CONTRACT_ADDRESS_INCREMENTS_DEFAULT
)


def test_alloc_fund_sender(pre: Alloc):
Expand Down
1 change: 1 addition & 0 deletions src/pytest_plugins/help/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def show_test_help(config):
"filler location",
"defining debug",
"pre-allocation behavior",
"testing of features",
]
elif pytest_ini.name in [
"pytest-consume.ini",
Expand Down