From 099ff5e0943c613240d6d7fd0d646f64bb9de6b6 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:37:16 +0200 Subject: [PATCH 1/2] chore(tests): Refactor mcopy_memory_expansion and others --- .../test_mcopy_memory_expansion.py | 52 +++++++++--------- .../eip7480_data_section/test_data_opcodes.py | 54 +------------------ 2 files changed, 26 insertions(+), 80 deletions(-) diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py index 77862fa58be..edb2b678ee1 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py @@ -8,24 +8,13 @@ import pytest -from ethereum_test_tools import Account, Bytecode, Environment +from ethereum_test_tools import Account, Alloc, Bytecode, Environment from ethereum_test_tools import Opcodes as Op -from ethereum_test_tools import ( - StateTestFiller, - Storage, - TestAddress, - Transaction, - cost_memory_bytes, -) +from ethereum_test_tools import StateTestFiller, Storage, Transaction, cost_memory_bytes +from ethereum_test_tools.common.base_types import Address from .common import REFERENCE_SPEC_GIT_PATH, REFERENCE_SPEC_VERSION -# Code address used to call the test bytecode on every test case. -caller_address = 0x100 - -# Code address used to perform the memory expansion. -memory_expansion_address = 0x200 - REFERENCE_SPEC_GIT_PATH = REFERENCE_SPEC_GIT_PATH REFERENCE_SPEC_VERSION = REFERENCE_SPEC_VERSION @@ -78,6 +67,7 @@ def subcall_exact_cost( def bytecode_storage( subcall_exact_cost: int, successful: bool, + memory_expansion_address: Address, ) -> Tuple[Bytecode, Storage.StorageDictType]: """ Prepares the bytecode and storage for the test, based on the expected result of the subcall @@ -126,26 +116,32 @@ def env( # noqa: D103 @pytest.fixture -def pre( # noqa: D103 - tx_max_fee_per_gas: int, - tx_gas_limit: int, - bytecode_storage: Tuple[bytes, Storage.StorageDictType], - callee_bytecode: bytes, -) -> Mapping: - return { - TestAddress: Account(balance=tx_max_fee_per_gas * tx_gas_limit), - caller_address: Account(code=bytecode_storage[0]), - memory_expansion_address: Account(code=callee_bytecode), - } +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: bytes) -> 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, @@ -155,7 +151,9 @@ def tx( # noqa: D103 @pytest.fixture -def post(bytecode_storage: Tuple[bytes, Storage.StorageDictType]) -> Mapping: # noqa: D103 +def post( # noqa: D103 + caller_address: Address, bytecode_storage: Tuple[bytes, Storage.StorageDictType] +) -> Mapping: return { caller_address: Account(storage=bytecode_storage[1]), } @@ -204,7 +202,7 @@ def post(bytecode_storage: Tuple[bytes, Storage.StorageDictType]) -> Mapping: # def test_mcopy_memory_expansion( state_test: StateTestFiller, env: Environment, - pre: Mapping[str, Account], + pre: Alloc, post: Mapping[str, Account], tx: Transaction, ): diff --git a/tests/prague/eip7692_eof_v1/eip7480_data_section/test_data_opcodes.py b/tests/prague/eip7692_eof_v1/eip7480_data_section/test_data_opcodes.py index a321046583f..196481cee61 100644 --- a/tests/prague/eip7692_eof_v1/eip7480_data_section/test_data_opcodes.py +++ b/tests/prague/eip7692_eof_v1/eip7480_data_section/test_data_opcodes.py @@ -1,12 +1,11 @@ """ -Execution of CALLF, RETF opcodes within EOF V1 containers tests +Execution of DATA* opcodes within EOF V1 containers tests """ import pytest from ethereum_test_tools import Account, Alloc, Environment, StateTestFiller, Transaction from ethereum_test_tools.eof.v1 import Container, Section -from ethereum_test_tools.eof.v1.constants import MAX_CODE_SECTIONS from ethereum_test_tools.vm.opcode import Opcodes as Op from .. import EOF_FORK_NAME @@ -16,57 +15,6 @@ pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) -contract_call_within_deep_nested_callf = Container( - name="contract_call_within_deep_nested_callf", - sections=[ - Section.Code( - code=(Op.CALLF[1] + Op.SSTORE(0, 1) + Op.STOP), - ) - ] - + [ - # All sections call next section and on return, store a 1 - # to their call stack height key - Section.Code( - code=(Op.CALLF[i] + Op.SSTORE(i - 1, 1) + Op.RETF), - code_inputs=0, - code_outputs=0, - ) - for i in range(2, MAX_CODE_SECTIONS) - ] - + [ - # Last section makes external contract call - Section.Code( - code=( - Op.EXTCALL(0x200, 0, 0, 0) + Op.SSTORE(MAX_CODE_SECTIONS - 1, Op.ISZERO) + Op.RETF - ), - code_inputs=0, - code_outputs=0, - ) - ], -) - -recursive_contract_call_within_deep_nested_callf = Container( - name="recursive_contract_call_within_deep_nested_callf", - sections=[ - # All sections call next section and on return, store a 1 - # to their call stack height key - Section.Code( - code=(Op.CALLF[i + 1] + Op.PUSH1(1) + Op.PUSH2(i) + Op.SSTORE + Op.STOP), - ) - for i in range(MAX_CODE_SECTIONS - 1) - ] - + [ - # Last section makes external contract call - Section.Code( - code=( - Op.SSTORE(MAX_CODE_SECTIONS - 1, Op.CALL(Op.GAS, 0x200, 0, 0, 0, 0, 0)) + Op.RETF - ), - code_inputs=0, - code_outputs=0, - ) - ], -) - def create_data_test(offset: int, datasize: int): """ From 63c85bc764c445ec70f4298eac6809cbb02072a1 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:35:12 +0200 Subject: [PATCH 2/2] new(tests): EOF - EIP-7480: Add tests for DATACOPY memory expansion --- docs/CHANGELOG.md | 1 + .../test_mcopy_memory_expansion.py | 2 +- .../test_datacopy_memory_expansion.py | 299 ++++++++++++++++++ 3 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 tests/prague/eip7692_eof_v1/eip7480_data_section/test_datacopy_memory_expansion.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f969bc00d44..5123b3c8393 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -24,6 +24,7 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Add tests for [EIP-3540: EOF - EVM Object Format v1](https://eips.ethereum.org/EIPS/eip-3540) ([#634](https://github.com/ethereum/execution-spec-tests/pull/634)). - 🔀 Update EIP-7002 tests to match spec changes in [ethereum/execution-apis#549](https://github.com/ethereum/execution-apis/pull/549) ([#600](https://github.com/ethereum/execution-spec-tests/pull/600)) - ✨ Convert a few eip1153 tests from ethereum/tests repo into .py ([#440](https://github.com/ethereum/execution-spec-tests/pull/440)). +- ✨ Add tests for [EIP-7480: EOF - Data section access instructions](https://eips.ethereum.org/EIPS/eip-7480) ([#518](https://github.com/ethereum/execution-spec-tests/pull/518), [#664](https://github.com/ethereum/execution-spec-tests/pull/664)). ### 🛠️ Framework diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py index edb2b678ee1..d855c9f8116 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py @@ -123,7 +123,7 @@ def caller_address( # noqa: D103 @pytest.fixture -def memory_expansion_address(pre: Alloc, callee_bytecode: bytes) -> Address: # noqa: D103 +def memory_expansion_address(pre: Alloc, callee_bytecode: Bytecode) -> Address: # noqa: D103 return pre.deploy_contract(code=callee_bytecode) diff --git a/tests/prague/eip7692_eof_v1/eip7480_data_section/test_datacopy_memory_expansion.py b/tests/prague/eip7692_eof_v1/eip7480_data_section/test_datacopy_memory_expansion.py new file mode 100644 index 00000000000..a8e35aea2bf --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip7480_data_section/test_datacopy_memory_expansion.py @@ -0,0 +1,299 @@ +""" +Memory expansion tests for DATACOPY +""" +from typing import Mapping, Tuple + +import pytest + +from ethereum_test_tools import ( + Account, + Alloc, + Bytecode, + Environment, + StateTestFiller, + Storage, + Transaction, +) +from ethereum_test_tools.common.base_types import Address +from ethereum_test_tools.common.helpers import cost_memory_bytes +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from .. import EOF_FORK_NAME + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7480.md" +REFERENCE_SPEC_VERSION = "3ee1334ef110420685f1c8ed63e80f9e1766c251" + +pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) + + +@pytest.fixture +def callee_bytecode(dest: int, src: int, length: int, data_section: bytes) -> Container: + """ + Callee performs a single datacopy operation and then returns. + """ + 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) + + # Perform the datacopy operation + bytecode += Op.DATACOPY(dest, src, length) + + bytecode += Op.RETURN + + return Container(sections=[Section.Code(code=bytecode), Section.Data(data=data_section)]) + + +@pytest.fixture +def subcall_exact_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. + """ + datacopy_cost = 3 + datacopy_cost += 3 * ((length + 31) // 32) + if length > 0 and dest + length > len(initial_memory): + datacopy_cost += cost_memory_bytes(dest + length, len(initial_memory)) + + calldatacopy_cost = 3 + calldatacopy_cost += 3 * ((len(initial_memory) + 31) // 32) + calldatacopy_cost += cost_memory_bytes(len(initial_memory), 0) + + pushes_cost = 3 * 7 + calldatasize_cost = 2 + return datacopy_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]: + """ + 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). + """ + 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: bytes) -> 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]), + } + + +@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.mark.parametrize("successful", [True, False]) +@pytest.mark.parametrize( + "initial_memory", + [ + bytes(range(0x00, 0x100)), + bytes(), + ], + ids=[ + "from_existent_memory", + "from_empty_memory", + ], +) +@pytest.mark.parametrize( + "data_section", + [ + bytes(), + b"\xfc", + bytes(range(0x00, 0x20)), + bytes(range(0x00, 0x100)), + ], + ids=["empty_data_section", "byte_data_section", "word_data_section", "large_data_section"], +) +def test_datacopy_memory_expansion( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + post: Mapping[str, Account], + tx: Transaction, +): + """ + Perform DATACOPY operations that expand the memory, and verify the gas it costs to do so. + """ + state_test( + env=env, + pre=pre, + post=post, + tx=tx, + ) + + +@pytest.mark.parametrize( + "dest,src,length", + [ + (2**256 - 1, 0x00, 0x01), + (2**256 - 2, 0x00, 0x01), + (2**255 - 1, 0x00, 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_length_expansion", + "max_length_minus_one_expansion", + "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]) +@pytest.mark.parametrize( + "initial_memory", + [ + bytes(range(0x00, 0x100)), + bytes(), + ], + ids=[ + "from_existent_memory", + "from_empty_memory", + ], +) +@pytest.mark.parametrize( + "data_section", + [ + bytes(), + b"\xfc", + bytes(range(0x00, 0x20)), + bytes(range(0x00, 0x100)), + ], + ids=["empty_data_section", "byte_data_section", "word_data_section", "large_data_section"], +) +def test_datacopy_huge_memory_expansion( + state_test: StateTestFiller, + env: Environment, + pre: Mapping[str, Account], + post: Mapping[str, Account], + tx: Transaction, +): + """ + Perform DATACOPY 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, + )