From e3d7ba2f4f60d133587437e0f9c214af33bda4b2 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 29 Jul 2024 21:06:44 +0000 Subject: [PATCH 1/4] feat(fw): Add gas test generator --- src/ethereum_test_tools/__init__.py | 3 + .../generators/__init__.py | 10 ++ src/ethereum_test_tools/generators/state.py | 163 ++++++++++++++++++ whitelist.txt | 2 + 4 files changed, 178 insertions(+) create mode 100644 src/ethereum_test_tools/generators/__init__.py create mode 100644 src/ethereum_test_tools/generators/state.py diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 4f9c51ead1..fa43e9bd55 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -78,6 +78,7 @@ Yul, YulCompiler, ) +from .generators import GasTestType, bytecode_gas_test __all__ = ( "SPEC_TYPES", @@ -108,6 +109,7 @@ "EOFTestFiller", "EVMCodeType", "FixtureCollector", + "GasTestType", "Hash", "Header", "Initcode", @@ -138,6 +140,7 @@ "Yul", "YulCompiler", "add_kzg_version", + "bytecode_gas_test", "call_return_code", "ceiling_division", "compute_create_address", diff --git a/src/ethereum_test_tools/generators/__init__.py b/src/ethereum_test_tools/generators/__init__.py new file mode 100644 index 0000000000..5269f1c2ef --- /dev/null +++ b/src/ethereum_test_tools/generators/__init__.py @@ -0,0 +1,10 @@ +""" +Ethereum test generators package. +""" + +from .state import GasTestType, bytecode_gas_test + +__all__ = [ + "GasTestType", + "bytecode_gas_test", +] diff --git a/src/ethereum_test_tools/generators/state.py b/src/ethereum_test_tools/generators/state.py new file mode 100644 index 0000000000..30e1e0395d --- /dev/null +++ b/src/ethereum_test_tools/generators/state.py @@ -0,0 +1,163 @@ +""" +Includes state test generators to use in Ethereum tests. +""" +from enum import Enum +from typing import Any, Callable, List + +import pytest + +from ethereum_test_base_types import Account +from ethereum_test_specs import StateTestFiller +from ethereum_test_types import Alloc, Environment, Transaction, eip_2028_transaction_data_cost +from ethereum_test_vm import Bytecode +from ethereum_test_vm import Opcodes as Op + +Decorator = Callable[[Callable[..., Any]], Callable[..., Any]] + + +class GasTestType(str, Enum): + """ + Enum to list the types of gas tests that can be generated. + """ + + EXACT_GAS = "exact_gas" + OOG = "oog" + + +def bytecode_gas_test( + *, + gas_test_types: List[GasTestType] | GasTestType = list(GasTestType), + with_data: bool = False, +) -> Decorator: + """ + Used to generate a parametrized test that checks that specified gas is + exactly the same as the gas used by the EVM. + + Automatically generates an exact-gas test, where the transaction is sent + with the exact amount of gas required, and an out-of-gas test, where the + transaction is sent with one less gas than required. + + Body of the decorated test is ignored. + + Required fixtures: + pre: Alloc - Automatically provided by filler. + state_test: StateTestFiller - Automatically provided by filler. + exact_gas_cost: int - Exact gas cost required to execute the bytecode. + bytecode: Bytecode, the bytecode to be tested. + + Optional fixtures: + data: bytes - if `with_data` is True, is the transaction calldata. + + Args: + gas_test_types: List[GasTestType] | GasTestType = list(GasTestType) - List of gas test types + to generate. + with_data: bool = False - If True, a `data` fixture needs to be defined and will be + included. + """ + if type(gas_test_types) is GasTestType: + gas_test_types = [gas_test_types] + + assert type(gas_test_types) is list + + def generator( + pre: Alloc, + state_test: StateTestFiller, + exact_gas_cost: int, + gas_test_type: GasTestType, + bytecode: Bytecode, + data: bytes = b"", + env: Environment = Environment(), + ): + """ + Generate a test case that can be further parametrized to verify gas comsumption of a + specific EVM bytecode. + """ + test_code = Op.SSTORE(0xBA5E, 1) + bytecode + Op.STOP() + test_code_address = pre.deploy_contract(code=test_code) + total_gas = ( + # Intrinsic tx cost + 21_000 + + 2_100 # Cold account access cost + + eip_2028_transaction_data_cost(data) + + 20_000 # Op.SSTORE + + 3 # Op.PUSH2 + + 3 # Op.PUSH1(1) + + exact_gas_cost + ) + tx = Transaction( + sender=pre.fund_eoa(), + to=test_code_address, + data=data, + gas_limit=total_gas if gas_test_type == GasTestType.EXACT_GAS else total_gas - 1, + ) + post = { + test_code_address: Account( + storage={0xBA5E: 1} if gas_test_type == GasTestType.EXACT_GAS else {} + ) + } + state_test( + env=env, + pre=pre, + post=post, + tx=tx, + ) + + if with_data: + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + @pytest.mark.parametrize( + "gas_test_type", + gas_test_types, + ids=lambda x: x.value, + ) + def generated_gas_test( + pre: Alloc, + state_test: StateTestFiller, + exact_gas_cost: int, + gas_test_type: GasTestType, + bytecode: Bytecode, + data: bytes, + ): + generator( + pre=pre, + state_test=state_test, + exact_gas_cost=exact_gas_cost, + gas_test_type=gas_test_type, + bytecode=bytecode, + data=data, + ) + + generated_gas_test.__name__ = func.__name__ + generated_gas_test.__doc__ = func.__doc__ + + return generated_gas_test + + else: + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + @pytest.mark.parametrize( + "gas_test_type", + gas_test_types, + ids=lambda x: x.value, + ) + def generated_gas_test( + pre: Alloc, + state_test: StateTestFiller, + exact_gas_cost: int, + gas_test_type: GasTestType, + bytecode: Bytecode, + ): + generator( + pre=pre, + state_test=state_test, + exact_gas_cost=exact_gas_cost, + gas_test_type=gas_test_type, + bytecode=bytecode, + ) + + generated_gas_test.__name__ = func.__name__ + generated_gas_test.__doc__ = func.__doc__ + + return generated_gas_test + + return decorator diff --git a/whitelist.txt b/whitelist.txt index 30c8e778d3..71e636f81d 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -179,6 +179,8 @@ globals go-ethereum's Golang gwei +h1 +h2 hacky hardfork hash32 From e944a8bf8f5ca5b624bbf802e27e7d6b3e3892a7 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 29 Jul 2024 21:06:59 +0000 Subject: [PATCH 2/4] refactor(tests): EIP-5656: Use gas test generator --- .../test_mcopy_memory_expansion.py | 237 ++++-------------- 1 file changed, 43 insertions(+), 194 deletions(-) diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py index 88d3b32472..b4777ee09c 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py @@ -4,13 +4,11 @@ that produce a memory expansion, and potentially an out-of-gas error. """ # noqa: E501 -from typing import Mapping, Tuple - import pytest -from ethereum_test_tools import Account, Address, Alloc, Bytecode, Environment +from ethereum_test_tools import Bytecode, GasTestType from ethereum_test_tools import Opcodes as Op -from ethereum_test_tools import StateTestFiller, Storage, Transaction, cost_memory_bytes +from ethereum_test_tools import bytecode_gas_test, cost_memory_bytes from .common import REFERENCE_SPEC_GIT_PATH, REFERENCE_SPEC_VERSION @@ -19,34 +17,28 @@ @pytest.fixture -def callee_bytecode(dest: int, src: int, length: int) -> Bytecode: +def bytecode(dest: int, src: int, length: int) -> Bytecode: """ - Callee performs a single mcopy operation and then returns. + Bytecode that performs a single mcopy operation with given parameters. """ - bytecode = Bytecode() - # Copy the initial memory - bytecode += Op.CALLDATACOPY(0x00, 0x00, Op.CALLDATASIZE()) - - # Pushes for the return operation - bytecode += Op.PUSH1(0x00) + Op.PUSH1(0x00) + bytecode = Op.CALLDATACOPY(0x00, 0x00, Op.CALLDATASIZE()) # Perform the mcopy operation bytecode += Op.MCOPY(dest, src, length) - bytecode += Op.RETURN - return bytecode @pytest.fixture -def subcall_exact_cost( +def exact_gas_cost( initial_memory: bytes, dest: int, length: int, ) -> int: """ - Returns the exact cost of the subcall, based on the initial memory and the length of the copy. + Returns the exact cost of the bytecode execution, based on the initial memory and the length + of the copy. """ mcopy_cost = 3 mcopy_cost += 3 * ((length + 31) // 32) @@ -57,222 +49,79 @@ def subcall_exact_cost( calldatacopy_cost += 3 * ((len(initial_memory) + 31) // 32) calldatacopy_cost += cost_memory_bytes(len(initial_memory), 0) - pushes_cost = 3 * 7 + pushes_cost = 3 * 5 calldatasize_cost = 2 return mcopy_cost + calldatacopy_cost + pushes_cost + calldatasize_cost @pytest.fixture -def bytecode_storage( - subcall_exact_cost: int, - successful: bool, - memory_expansion_address: Address, -) -> Tuple[Bytecode, Storage.StorageDictType]: +def data(initial_memory: bytes) -> bytes: """ - Prepares the bytecode and storage for the test, based on the expected result of the subcall - (whether it succeeds or fails depending on the length of the memory expansion). + Fixture rename for test case generator. """ - bytecode = Bytecode() - storage = {} - - # Pass on the calldata - bytecode += Op.CALLDATACOPY(0x00, 0x00, Op.CALLDATASIZE()) - - subcall_gas = subcall_exact_cost if successful else subcall_exact_cost - 1 - - # Perform the subcall and store a one in the result location - bytecode += Op.SSTORE( - Op.CALL(subcall_gas, memory_expansion_address, 0, 0, Op.CALLDATASIZE(), 0, 0), 1 - ) - storage[int(successful)] = 1 - - return (bytecode, storage) - - -@pytest.fixture -def tx_max_fee_per_gas() -> int: # noqa: D103 - return 7 - - -@pytest.fixture -def block_gas_limit() -> int: # noqa: D103 - return 100_000_000 - - -@pytest.fixture -def tx_gas_limit( # noqa: D103 - subcall_exact_cost: int, - block_gas_limit: int, -) -> int: - return min(max(500_000, subcall_exact_cost * 2), block_gas_limit) - - -@pytest.fixture -def env( # noqa: D103 - block_gas_limit: int, -) -> Environment: - return Environment(gas_limit=block_gas_limit) - - -@pytest.fixture -def caller_address( # noqa: D103 - pre: Alloc, bytecode_storage: Tuple[bytes, Storage.StorageDictType] -) -> Address: - return pre.deploy_contract(code=bytecode_storage[0]) - - -@pytest.fixture -def memory_expansion_address(pre: Alloc, callee_bytecode: Bytecode) -> Address: # noqa: D103 - return pre.deploy_contract(code=callee_bytecode) - - -@pytest.fixture -def sender(pre: Alloc, tx_max_fee_per_gas: int, tx_gas_limit: int) -> Address: # noqa: D103 - return pre.fund_eoa(tx_max_fee_per_gas * tx_gas_limit) - - -@pytest.fixture -def tx( # noqa: D103 - sender: Address, - caller_address: Address, - initial_memory: bytes, - tx_max_fee_per_gas: int, - tx_gas_limit: int, -) -> Transaction: - return Transaction( - sender=sender, - to=caller_address, - data=initial_memory, - gas_limit=tx_gas_limit, - max_fee_per_gas=tx_max_fee_per_gas, - max_priority_fee_per_gas=0, - ) - - -@pytest.fixture -def post( # noqa: D103 - caller_address: Address, bytecode_storage: Tuple[bytes, Storage.StorageDictType] -) -> Mapping: - return { - caller_address: Account(storage=bytecode_storage[1]), - } + return initial_memory @pytest.mark.parametrize( "dest,src,length", [ - (0x00, 0x00, 0x01), - (0x100, 0x00, 0x01), - (0x1F, 0x00, 0x01), - (0x20, 0x00, 0x01), - (0x1000, 0x00, 0x01), - (0x1000, 0x00, 0x40), - (0x00, 0x00, 0x00), - (2**256 - 1, 0x00, 0x00), - (0x00, 2**256 - 1, 0x00), - (2**256 - 1, 2**256 - 1, 0x00), - ], - ids=[ - "single_byte_expansion", - "single_byte_expansion_2", - "single_byte_expansion_word_boundary", - "single_byte_expansion_word_boundary_2", - "multi_word_expansion", - "multi_word_expansion_2", - "zero_length_expansion", - "huge_dest_zero_length", - "huge_src_zero_length", - "huge_dest_huge_src_zero_length", + pytest.param(0x00, 0x00, 0x01, id="single_byte_expansion"), + pytest.param(0x100, 0x00, 0x01, id="single_byte_expansion_2"), + pytest.param(0x1F, 0x00, 0x01, id="single_byte_expansion_word_boundary"), + pytest.param(0x20, 0x00, 0x01, id="single_byte_expansion_word_boundary_2"), + pytest.param(0x1000, 0x00, 0x01, id="multi_word_expansion"), + pytest.param(0x1000, 0x00, 0x40, id="multi_word_expansion_2"), + pytest.param(0x00, 0x00, 0x00, id="zero_length_expansion"), + pytest.param(2**256 - 1, 0x00, 0x00, id="huge_dest_zero_length"), + pytest.param(0x00, 2**256 - 1, 0x00, id="huge_src_zero_length"), + pytest.param(2**256 - 1, 2**256 - 1, 0x00, id="huge_dest_huge_src_zero_length"), ], ) -@pytest.mark.parametrize("successful", [True, False]) @pytest.mark.parametrize( "initial_memory", [ - bytes(range(0x00, 0x100)), - bytes(), - ], - ids=[ - "from_existent_memory", - "from_empty_memory", + pytest.param(bytes(range(0x00, 0x100)), id="from_existent_memory"), + pytest.param(bytes(), id="from_empty_memory"), ], ) @pytest.mark.valid_from("Cancun") -def test_mcopy_memory_expansion( - state_test: StateTestFiller, - env: Environment, - pre: Alloc, - post: Mapping[str, Account], - tx: Transaction, -): +@bytecode_gas_test(with_data=True) +def test_mcopy_memory_expansion_gas(): """ Perform MCOPY operations that expand the memory, and verify the gas it costs to do so. """ - state_test( - env=env, - pre=pre, - post=post, - tx=tx, - ) + pass @pytest.mark.parametrize( "dest,src,length", [ - (2**256 - 1, 0x00, 0x01), - (2**256 - 2, 0x00, 0x01), - (2**255 - 1, 0x00, 0x01), - (0x00, 2**256 - 1, 0x01), - (0x00, 2**256 - 2, 0x01), - (0x00, 2**255 - 1, 0x01), - (0x00, 0x00, 2**256 - 1), - (0x00, 0x00, 2**256 - 2), - (0x00, 0x00, 2**255 - 1), - ], - ids=[ - "max_dest_single_byte_expansion", - "max_dest_minus_one_single_byte_expansion", - "half_max_dest_single_byte_expansion", - "max_src_single_byte_expansion", - "max_src_minus_one_single_byte_expansion", - "half_max_src_single_byte_expansion", - "max_length_expansion", - "max_length_minus_one_expansion", - "half_max_length_expansion", + pytest.param(2**256 - 1, 0x00, 0x01, id="max_dest_single_byte_expansion"), + pytest.param(2**256 - 2, 0x00, 0x01, id="max_dest_minus_one_single_byte_expansion"), + pytest.param(2**255 - 1, 0x00, 0x01, id="half_max_dest_single_byte_expansion"), + pytest.param(0x00, 2**256 - 1, 0x01, id="max_src_single_byte_expansion"), + pytest.param(0x00, 2**256 - 2, 0x01, id="max_src_minus_one_single_byte_expansion"), + pytest.param(0x00, 2**255 - 1, 0x01, id="half_max_src_single_byte_expansion"), + pytest.param(0x00, 0x00, 2**256 - 1, id="max_length_expansion"), + pytest.param(0x00, 0x00, 2**256 - 2, id="max_length_minus_one_expansion"), + pytest.param(0x00, 0x00, 2**255 - 1, id="half_max_length_expansion"), ], ) @pytest.mark.parametrize( - "subcall_exact_cost", - [2**128 - 1], - ids=[""], -) # Limit subcall gas, otherwise it would be impossibly large -@pytest.mark.parametrize("successful", [False]) + "exact_gas_cost", [pytest.param(30_000_000, id="")] +) # Hard-code gas, otherwise it would be impossibly large @pytest.mark.parametrize( "initial_memory", [ - bytes(range(0x00, 0x100)), - bytes(), - ], - ids=[ - "from_existent_memory", - "from_empty_memory", + pytest.param(bytes(range(0x00, 0x100)), id="from_existent_memory"), + pytest.param(bytes(), id="from_empty_memory"), ], ) @pytest.mark.valid_from("Cancun") -def test_mcopy_huge_memory_expansion( - state_test: StateTestFiller, - env: Environment, - pre: Mapping[str, Account], - post: Mapping[str, Account], - tx: Transaction, -): +@bytecode_gas_test(gas_test_types=GasTestType.OOG, with_data=True) +def test_mcopy_huge_memory_expansion(): """ Perform MCOPY operations that expand the memory by huge amounts, and verify that it correctly runs out of gas. """ - state_test( - env=env, - pre=pre, - post=post, - tx=tx, - ) + pass From b1430c09fea1b354c375ade6db921e18d569c8ef Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 21 Aug 2024 18:52:13 +0000 Subject: [PATCH 3/4] refactor(tools): rename test generator --- src/ethereum_test_tools/__init__.py | 4 ++-- src/ethereum_test_tools/generators/__init__.py | 4 ++-- src/ethereum_test_tools/generators/state.py | 2 +- tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index fa43e9bd55..12bd4238d2 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -78,7 +78,7 @@ Yul, YulCompiler, ) -from .generators import GasTestType, bytecode_gas_test +from .generators import GasTestType, exact_gas_test __all__ = ( "SPEC_TYPES", @@ -140,7 +140,6 @@ "Yul", "YulCompiler", "add_kzg_version", - "bytecode_gas_test", "call_return_code", "ceiling_division", "compute_create_address", @@ -150,5 +149,6 @@ "cost_memory_bytes", "eip_2028_transaction_data_cost", "eip_2028_transaction_data_cost", + "exact_gas_test", "vm", ) diff --git a/src/ethereum_test_tools/generators/__init__.py b/src/ethereum_test_tools/generators/__init__.py index 5269f1c2ef..2fb10b4c90 100644 --- a/src/ethereum_test_tools/generators/__init__.py +++ b/src/ethereum_test_tools/generators/__init__.py @@ -2,9 +2,9 @@ Ethereum test generators package. """ -from .state import GasTestType, bytecode_gas_test +from .state import GasTestType, exact_gas_test __all__ = [ "GasTestType", - "bytecode_gas_test", + "exact_gas_test", ] diff --git a/src/ethereum_test_tools/generators/state.py b/src/ethereum_test_tools/generators/state.py index 30e1e0395d..7078f8c1e9 100644 --- a/src/ethereum_test_tools/generators/state.py +++ b/src/ethereum_test_tools/generators/state.py @@ -24,7 +24,7 @@ class GasTestType(str, Enum): OOG = "oog" -def bytecode_gas_test( +def exact_gas_test( *, gas_test_types: List[GasTestType] | GasTestType = list(GasTestType), with_data: bool = False, diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py index b4777ee09c..29591f1239 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py @@ -8,7 +8,7 @@ from ethereum_test_tools import Bytecode, GasTestType from ethereum_test_tools import Opcodes as Op -from ethereum_test_tools import bytecode_gas_test, cost_memory_bytes +from ethereum_test_tools import cost_memory_bytes, exact_gas_test from .common import REFERENCE_SPEC_GIT_PATH, REFERENCE_SPEC_VERSION @@ -85,7 +85,7 @@ def data(initial_memory: bytes) -> bytes: ], ) @pytest.mark.valid_from("Cancun") -@bytecode_gas_test(with_data=True) +@exact_gas_test(with_data=True) def test_mcopy_memory_expansion_gas(): """ Perform MCOPY operations that expand the memory, and verify the gas it costs to do so. @@ -118,7 +118,7 @@ def test_mcopy_memory_expansion_gas(): ], ) @pytest.mark.valid_from("Cancun") -@bytecode_gas_test(gas_test_types=GasTestType.OOG, with_data=True) +@exact_gas_test(gas_test_types=GasTestType.OOG, with_data=True) def test_mcopy_huge_memory_expansion(): """ Perform MCOPY operations that expand the memory by huge amounts, and verify that it correctly From 5f35889d459e9e165b3a9ab9bd17635f6027c511 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 21 Aug 2024 18:52:43 +0000 Subject: [PATCH 4/4] new(tests): EIP-5656 add eof marker to memory expansion test --- tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py index 29591f1239..20f266f9a9 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py @@ -84,6 +84,7 @@ def data(initial_memory: bytes) -> bytes: pytest.param(bytes(), id="from_empty_memory"), ], ) +@pytest.mark.with_all_evm_code_types @pytest.mark.valid_from("Cancun") @exact_gas_test(with_data=True) def test_mcopy_memory_expansion_gas(): @@ -117,6 +118,7 @@ def test_mcopy_memory_expansion_gas(): pytest.param(bytes(), id="from_empty_memory"), ], ) +@pytest.mark.with_all_evm_code_types @pytest.mark.valid_from("Cancun") @exact_gas_test(gas_test_types=GasTestType.OOG, with_data=True) def test_mcopy_huge_memory_expansion():