diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 3db7d748dc..c382556121 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -36,8 +36,8 @@ add_kzg_version, ceiling_division, compute_create2_address, - compute_create3_address, compute_create_address, + compute_eofcreate_address, copy_opcode_cost, cost_memory_bytes, eip_2028_transaction_data_cost, @@ -119,7 +119,7 @@ "ceiling_division", "compute_create_address", "compute_create2_address", - "compute_create3_address", + "compute_eofcreate_address", "copy_opcode_cost", "cost_memory_bytes", "eip_2028_transaction_data_cost", diff --git a/src/ethereum_test_tools/common/__init__.py b/src/ethereum_test_tools/common/__init__.py index fd84bb2364..d953fb6111 100644 --- a/src/ethereum_test_tools/common/__init__.py +++ b/src/ethereum_test_tools/common/__init__.py @@ -28,8 +28,8 @@ add_kzg_version, ceiling_division, compute_create2_address, - compute_create3_address, compute_create_address, + compute_eofcreate_address, copy_opcode_cost, cost_memory_bytes, eip_2028_transaction_data_cost, @@ -83,7 +83,7 @@ "ceiling_division", "compute_create_address", "compute_create2_address", - "compute_create3_address", + "compute_eofcreate_address", "copy_opcode_cost", "cost_memory_bytes", "eip_2028_transaction_data_cost", diff --git a/src/ethereum_test_tools/common/helpers.py b/src/ethereum_test_tools/common/helpers.py index 2d8c9b8b53..c463b18661 100644 --- a/src/ethereum_test_tools/common/helpers.py +++ b/src/ethereum_test_tools/common/helpers.py @@ -71,14 +71,13 @@ def copy_opcode_cost(length: int) -> int: return 3 + (ceiling_division(length, 32) * 3) + cost_memory_bytes(length, 0) -def compute_create3_address( +def compute_eofcreate_address( address: FixedSizeBytesConvertible, salt: FixedSizeBytesConvertible, init_container: BytesConvertible, ) -> Address: """ - Compute address of the resulting contract created using the `CREATE3` - opcode. + Compute address of the resulting contract created using the `EOFCREATE` opcode. """ hash = keccak256(b"\xff" + Address(address) + Hash(salt) + keccak256(Bytes(init_container))) return Address(hash[-20:]) diff --git a/src/ethereum_test_tools/eof/v1/__init__.py b/src/ethereum_test_tools/eof/v1/__init__.py index e54a72d31e..66eb8c913f 100644 --- a/src/ethereum_test_tools/eof/v1/__init__.py +++ b/src/ethereum_test_tools/eof/v1/__init__.py @@ -441,14 +441,12 @@ def init_container(self) -> Container: """ return Container( sections=[ - Section( - kind=SectionKind.CODE, - data=Op.RETURNCONTRACT(0, 0, 0), + Section.Code( + code=Op.RETURNCONTRACT[0](0, 0), max_stack_height=2, ), - Section( - kind=SectionKind.CONTAINER, - data=bytes(self.deploy_container), + Section.Container( + container=self.deploy_container, ), ], ) @@ -456,19 +454,17 @@ def init_container(self) -> Container: @cached_property def bytecode(self) -> bytes: """ - Generate legacy initcode that inits a contract with the specified code. - The initcode can be padded to a specified length for testing purposes. + Generate an EOF container performs `EOFCREATE` with the specified code. """ initcode = Container( sections=[ - Section( - data=Op.CREATE3(0, 0, 0, 0, len(self.deploy_container)) + Op.STOP(), - kind=SectionKind.CODE, + Section.Code( + # TODO: Pass calldata + code=Op.EOFCREATE[0](0, 0, 0, 0) + Op.STOP(), max_stack_height=4, ), - Section( - kind=SectionKind.CONTAINER, - data=self.init_container, + Section.Container( + container=self.init_container, ), ] ) diff --git a/src/ethereum_test_tools/vm/opcode.py b/src/ethereum_test_tools/vm/opcode.py index 05cbf51670..bb3209babf 100644 --- a/src/ethereum_test_tools/vm/opcode.py +++ b/src/ethereum_test_tools/vm/opcode.py @@ -5091,11 +5091,11 @@ class Opcodes(Opcode, Enum): """ - CREATE3 = Opcode(0xEC, popped_stack_items=4, pushed_stack_items=1, data_portion_length=1) + EOFCREATE = Opcode(0xEC, popped_stack_items=4, pushed_stack_items=1, data_portion_length=1) """ !!! Note: This opcode is under development - CREATE3() + EOFCREATE[initcontainer_index](value, salt, input_offset, input_size) ---- Description diff --git a/tests/prague/eip7692_eof_v1/eip3540_eof_v1/test_code_validation.py b/tests/prague/eip7692_eof_v1/eip3540_eof_v1/test_code_validation.py index f0bb8b8d36..efbb6ce650 100644 --- a/tests/prague/eip7692_eof_v1/eip3540_eof_v1/test_code_validation.py +++ b/tests/prague/eip7692_eof_v1/eip3540_eof_v1/test_code_validation.py @@ -14,7 +14,7 @@ EOFTestFiller, TestAddress, Transaction, - compute_create3_address, + compute_eofcreate_address, ) from ethereum_test_tools.eof.v1 import Container, Initcode @@ -103,7 +103,7 @@ def post( # noqa: D103 container: Container, create3_opcode_contract_address: str, ) -> Dict[Address, Account]: - create_opcode_created_contract_address = compute_create3_address( + create_opcode_created_contract_address = compute_eofcreate_address( create3_opcode_contract_address, 0, bytes(create3_init_container.init_container), diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/__init__.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/__init__.py new file mode 100644 index 0000000000..4655d79f8d --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/__init__.py @@ -0,0 +1,14 @@ +""" +EOFCREATE, RETURNCONTRACT, and container tests + +evmone tests not ported + +create_tx_with_eof_initcode - This calls it invalid, it is now the way to add EOF contacts to state +eofcreate_extcall_returncontract - per the new initcode mode tests you cannot have RETURNCONTRACT + in a deployed contract +eofcreate_dataloadn_referring_to_auxdata - covered by + tests.prague.eip7480_data_section.test_data_opcodes.test_data_section_succeed +eofcreate_initcontainer_return - RETURN is banned in initcode containers +eofcreate_initcontainer_stop - STOP is banned in initcode containers +All TXCREATE tests - TXCREATE has been removed from Prague +""" diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/helpers.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/helpers.py new file mode 100644 index 0000000000..8e6b638323 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/helpers.py @@ -0,0 +1,88 @@ +""" +A collection of contracts used in 7620 EOF tests +""" +import itertools + +from ethereum_test_tools import Address +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import Transaction +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.eof.v1.constants import NON_RETURNING_SECTION + +"""Storage addresses for common testing fields""" +_slot = itertools.count() +next(_slot) # don't use slot 0 +slot_code_worked = next(_slot) +slot_code_should_fail = next(_slot) +slot_create_address = next(_slot) +slot_calldata = next(_slot) +slot_call_result = next(_slot) +slot_returndata = next(_slot) +slot_returndata_size = next(_slot) + +slot_last_slot = next(_slot) + +value_code_worked = 0x2015 +value_canary_should_not_change = 0x2019 +value_canary_to_be_overwritten = 0x2009 +value_create_failed = 0 +value_call_result_success = 0 + +smallest_runtime_subcontainer = Container( + name="Runtime Subcontainer", + sections=[ + Section.Code( + code=Op.STOP, code_inputs=0, code_outputs=NON_RETURNING_SECTION, max_stack_height=0 + ) + ], +) + +smallest_initcode_subcontainer = Container( + name="Initcode Subcontainer", + sections=[ + Section.Code( + code=Op.RETURNCONTRACT[0](0, 0), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ), + Section.Container(container=smallest_runtime_subcontainer), + ], +) + + +def fixed_address(index: int) -> Address: + """ + Returns an determinstic address for testing + Parameters + ---------- + index - how foar off of the initial to create the address + + Returns + ------- + An address, unique per index and human friendly for testing + + """ + return Address(0x7E570000 + index) + + +default_address = fixed_address(0) + + +def simple_transaction( + target: Address = default_address, payload: bytes = b"", gas_limit: int = 10_000_000 +): + """ + Creates a simple transaction + Parameters + ---------- + target the target address, defaults to 0x100 + payload the payload, defauls to empty + + Returns + ------- + a transaction instance that can be passed into state_tests + """ + return Transaction( + nonce=1, to=target, gas_limit=gas_limit, gas_price=10, protected=False, data=payload + ) diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/spec.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/spec.py new file mode 100644 index 0000000000..7bf760554f --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/spec.py @@ -0,0 +1,5 @@ +""" +EOF V1 Constants used throughout all tests +""" + +EOF_FORK_NAME = "Prague" diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_eofcreate.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_eofcreate.py new file mode 100644 index 0000000000..e403e9334b --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_eofcreate.py @@ -0,0 +1,510 @@ +""" +Test good and bad EOFCREATE cases +""" + +import pytest + +from ethereum_test_tools import ( + Account, + Environment, + StateTestFiller, + TestAddress, + compute_eofcreate_address, +) +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.eof.v1.constants import NON_RETURNING_SECTION +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from .helpers import ( + default_address, + fixed_address, + simple_transaction, + slot_call_result, + slot_calldata, + slot_code_worked, + slot_create_address, + slot_last_slot, + slot_returndata_size, + smallest_initcode_subcontainer, + smallest_runtime_subcontainer, + value_call_result_success, + value_canary_to_be_overwritten, + value_code_worked, + value_create_failed, +) +from .spec import EOF_FORK_NAME + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7620.md" +REFERENCE_SPEC_VERSION = "52ddbcdddcf72dd72427c319f2beddeb468e1737" + +pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) + + +def test_simple_eofcreate( + state_test: StateTestFiller, +): + """ + Verifies a simple EOFCREATE case + """ + env = Environment() + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(0, Op.EOFCREATE[0](0, 0, 0, 0)) + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=smallest_initcode_subcontainer), + ], + data=b"abcdef", + ), + storage={0: 0xB17D}, # a canary to be overwritten + ), + } + # Storage in 0 should have the address, + post = { + default_address: Account( + storage={ + 0: compute_eofcreate_address(default_address, 0, smallest_initcode_subcontainer) + } + ) + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +def test_eofcreate_then_call( + state_test: StateTestFiller, +): + """ + Verifies a simple EOFCREATE case, and then calls the deployed contract + """ + env = Environment() + callable_contract = Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_code_worked, value_code_worked) + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ), + ] + ) + callable_contract_initcode = Container( + sections=[ + Section.Code( + code=Op.RETURNCONTRACT[0](0, 0), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ), + Section.Container(container=callable_contract), + ] + ) + + callable_address = compute_eofcreate_address(default_address, 0, callable_contract_initcode) + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.EXTCALL(Op.SLOAD(slot_create_address), 0, 0, 0) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=callable_contract_initcode), + ], + ) + ), + } + # Storage in 0 should have the address, + # + post = { + default_address: Account( + storage={slot_create_address: callable_address, slot_code_worked: value_code_worked} + ), + callable_address: Account(storage={slot_code_worked: value_code_worked}), + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +@pytest.mark.parametrize( + "auxdata_bytes", + [ + pytest.param(b"", id="zero"), + pytest.param(b"aabbcc", id="short"), + pytest.param(b"aabbccddeef", id="one_byte_short"), + pytest.param(b"aabbccddeeff", id="exact"), + pytest.param(b"aabbccddeeffg", id="one_byte_long"), + pytest.param(b"aabbccddeeffgghhii", id="extra"), + ], +) +def test_auxdata_variations(state_test: StateTestFiller, auxdata_bytes: bytes): + """ + Verifies that auxdata bytes are correctly handled in RETURNCONTRACT + """ + env = Environment() + auxdata_size = len(auxdata_bytes) + pre_deploy_header_data_size = 18 + pre_deploy_data = b"AABBCC" + deploy_success = len(auxdata_bytes) + len(pre_deploy_data) >= pre_deploy_header_data_size + + runtime_subcontainer = Container( + name="Runtime Subcontainer with truncated data", + sections=[ + Section.Code( + code=Op.STOP, code_inputs=0, code_outputs=NON_RETURNING_SECTION, max_stack_height=0 + ), + Section.Data(data=pre_deploy_data, custom_size=pre_deploy_header_data_size), + ], + ) + + initcode_subcontainer = Container( + name="Initcode Subcontainer", + sections=[ + Section.Code( + code=Op.MSTORE(0, Op.PUSH32(auxdata_bytes.ljust(32, b"\0"))) + + Op.RETURNCONTRACT[0](0, auxdata_size), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ), + Section.Container(container=runtime_subcontainer), + ], + ) + + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=initcode_subcontainer), + ] + ), + storage={slot_create_address: value_canary_to_be_overwritten}, + ), + } + + # Storage in 0 should have the address, + post = { + default_address: Account( + storage={ + slot_create_address: compute_eofcreate_address( + default_address, 0, initcode_subcontainer + ) + if deploy_success + else b"\0" + } + ) + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +def test_calldata(state_test: StateTestFiller): + """ + Verifies CALLDATA passing through EOFCREATE + """ + env = Environment() + + initcode_subcontainer = Container( + name="Initcode Subcontainer", + sections=[ + Section.Code( + code=Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE(slot_calldata, Op.MLOAD(0)) + + Op.RETURNCONTRACT[0](0, Op.CALLDATASIZE), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=3, + ), + Section.Container(container=smallest_runtime_subcontainer), + ], + ) + + calldata_size = 32 + calldata = b"\x45" * calldata_size + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.MSTORE(0, Op.PUSH32(calldata)) + + Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, calldata_size)) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=initcode_subcontainer), + ] + ) + ), + } + + # deployed contract is smallest plus data + deployed_contract = Container( + name="deployed contract", + sections=[ + *smallest_runtime_subcontainer.sections, + Section.Data(data=calldata), + ], + ) + # factory contract Storage in 0 should have the created address, + # created contract storage in 0 should have the calldata + created_address = compute_eofcreate_address(default_address, 0, initcode_subcontainer) + post = { + default_address: Account(storage={slot_create_address: created_address}), + created_address: Account(code=deployed_contract, storage={slot_calldata: calldata}), + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +def test_eofcreate_in_initcode( + state_test: StateTestFiller, +): + """ + Verifies an EOFCREATE occuring within initcode creates that contract + """ + nested_initcode_subcontainer = Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.RETURNCONTRACT[1](0, 0), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=smallest_initcode_subcontainer), + Section.Container(container=smallest_runtime_subcontainer), + ] + ) + + env = Environment() + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=nested_initcode_subcontainer), + ] + ) + ), + } + + outer_address = compute_eofcreate_address(default_address, 0, nested_initcode_subcontainer) + inner_address = compute_eofcreate_address(outer_address, 0, smallest_initcode_subcontainer) + post = { + default_address: Account( + storage={slot_create_address: outer_address, slot_code_worked: value_code_worked} + ), + outer_address: Account( + storage={slot_create_address: inner_address, slot_code_worked: value_code_worked} + ), + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +def test_eofcreate_in_initcode_reverts( + state_test: StateTestFiller, +): + """ + Verifies an EOFCREATE occuring in an initcode is rolled back when the initcode reverts + """ + nested_initcode_subcontainer = Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.REVERT(0, 0), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=smallest_initcode_subcontainer), + Section.Container(container=smallest_runtime_subcontainer), + ] + ) + + env = Environment() + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=nested_initcode_subcontainer), + ] + ), + storage={slot_create_address: value_canary_to_be_overwritten}, + ), + } + + outer_address = compute_eofcreate_address(default_address, 0, nested_initcode_subcontainer) + inner_address = compute_eofcreate_address(outer_address, 0, smallest_initcode_subcontainer) + post = { + default_address: Account( + storage={ + slot_create_address: 0, + slot_code_worked: value_code_worked, + } + ), + outer_address: Account.NONEXISTENT, + inner_address: Account.NONEXISTENT, + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +def test_return_data_cleared( + state_test: StateTestFiller, +): + """ + Verifies the return data is not re-used from a extcall but is cleared upon eofcreate + """ + env = Environment() + callable_address = fixed_address(1) + value_return_canary = 0x4158675309 + value_return_canary_size = 5 + callable_contract = Container( + sections=[ + Section.Code( + code=Op.MSTORE(0, value_return_canary) + Op.RETURN(0, value_return_canary_size), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ) + ] + ) + + slot_returndata_size_2 = slot_last_slot * 2 + slot_returndata_size + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_call_result, Op.EXTCALL(callable_address, 0, 0, 0)) + + Op.SSTORE(slot_returndata_size, Op.RETURNDATASIZE) + + Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_returndata_size_2, Op.RETURNDATASIZE) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=smallest_initcode_subcontainer), + ], + ) + ), + callable_address: Account(code=callable_contract, nonce=1), + } + + new_contract_address = compute_eofcreate_address( + default_address, 0, smallest_initcode_subcontainer + ) + post = { + default_address: Account( + storage={ + slot_call_result: value_call_result_success, + slot_returndata_size: value_return_canary_size, + slot_create_address: new_contract_address, + slot_returndata_size_2: 0, + slot_code_worked: value_code_worked, + }, + nonce=1, + ), + callable_address: Account(nonce=1), + new_contract_address: Account(nonce=1), + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +def test_address_collision( + state_test: StateTestFiller, +): + """ + Verifies a simple EOFCREATE case + """ + env = Environment() + + salt_zero_address = compute_eofcreate_address( + default_address, 0, smallest_initcode_subcontainer + ) + salt_one_address = compute_eofcreate_address( + default_address, 1, smallest_initcode_subcontainer + ) + + slot_create_address_2 = slot_last_slot * 2 + slot_create_address + slot_create_address_3 = slot_last_slot * 3 + slot_create_address + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_create_address_2, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_create_address_3, Op.EOFCREATE[0](0, 1, 0, 0)) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=smallest_initcode_subcontainer), + ], + ) + ), + salt_one_address: Account(balance=1, nonce=1), + } + post = { + default_address: Account( + storage={ + slot_create_address: salt_zero_address, + slot_create_address_2: value_create_failed, # had an in-transaction collision + slot_create_address_3: value_create_failed, # had a pre-existing collision + slot_code_worked: value_code_worked, + } + ) + } + + # Multiple create fails is expensive, use an absurd amount of gas + state_test(env=env, pre=pre, post=post, tx=simple_transaction(gas_limit=300_000_000_000)) diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_eofcreate_failures.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_eofcreate_failures.py new file mode 100644 index 0000000000..9d3b3740bb --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_eofcreate_failures.py @@ -0,0 +1,557 @@ +""" +Test good and bad EOFCREATE cases +""" + +import pytest + +from ethereum_test_tools import ( + Account, + Environment, + StateTestFiller, + TestAddress, + compute_eofcreate_address, +) +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.eof.v1.constants import ( + MAX_BYTECODE_SIZE, + MAX_INITCODE_SIZE, + NON_RETURNING_SECTION, +) +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from .helpers import ( + default_address, + simple_transaction, + slot_code_should_fail, + slot_code_worked, + slot_create_address, + slot_returndata, + slot_returndata_size, + smallest_initcode_subcontainer, + smallest_runtime_subcontainer, + value_canary_should_not_change, + value_code_worked, + value_create_failed, +) +from .spec import EOF_FORK_NAME + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7620.md" +REFERENCE_SPEC_VERSION = "52ddbcdddcf72dd72427c319f2beddeb468e1737" + +pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) + + +@pytest.mark.parametrize( + "revert", + [ + pytest.param(b"", id="empty"), + pytest.param(b"\x08\xc3\x79\xa0", id="Error(string)"), + ], +) +def test_initcode_revert(state_test: StateTestFiller, revert: bytes): + """ + Verifies proper handling of REVERT in initcode + """ + env = Environment() + revert_size = len(revert) + + initcode_subcontainer = Container( + name="Initcode Subcontainer that reverts", + sections=[ + Section.Code( + code=Op.MSTORE(0, Op.PUSH32(revert)) + Op.REVERT(32 - revert_size, revert_size), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ), + ], + ) + + factory_contract = Container( + name="factory contract", + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_returndata_size, Op.RETURNDATASIZE) + + Op.RETURNDATACOPY(Op.SUB(32, Op.RETURNDATASIZE), 0, Op.RETURNDATASIZE) + + Op.SSTORE(slot_returndata, Op.MLOAD(0)) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=initcode_subcontainer), + ], + ) + + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account(code=factory_contract), + } + + post = { + default_address: Account( + storage={ + slot_create_address: value_create_failed, + slot_returndata_size: revert_size, + slot_returndata: revert, + slot_code_worked: value_code_worked, + } + ) + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +def test_initcode_aborts( + state_test: StateTestFiller, +): + """ + Verifies correct handling of a halt in EOF initcode + """ + env = Environment() + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container( + container=Container( + sections=[ + Section.Code( + code=Op.INVALID, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=0, + ) + ] + ) + ), + ] + ) + ), + } + # Storage in slot_create_address should not have the address, + post = { + default_address: Account( + storage={ + slot_create_address: value_create_failed, + slot_code_worked: value_code_worked, + } + ) + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +""" +Size of the factory portion of test_eofcreate_deploy_sizes, but as the runtime code is dynamic, we +have to use a pre-calculated size +""" +factory_size = 30 + + +@pytest.mark.parametrize( + "target_deploy_size", + [ + pytest.param(0x4000, id="large"), + pytest.param(MAX_BYTECODE_SIZE, id="max"), + pytest.param(MAX_BYTECODE_SIZE + 1, id="overmax"), + pytest.param(MAX_INITCODE_SIZE - factory_size, id="initcodemax"), + pytest.param(MAX_INITCODE_SIZE - factory_size + 1, id="initcodeovermax"), + pytest.param(0xFFFF - factory_size, id="64k-1"), + ], +) +def test_eofcreate_deploy_sizes( + state_test: StateTestFiller, + target_deploy_size: int, +): + """ + Verifies a mix of runtime contract sizes mixing success and multiple size failure modes. + """ + env = Environment() + + runtime_container = Container( + sections=[ + Section.Code( + code=Op.JUMPDEST * (target_deploy_size - len(smallest_runtime_subcontainer)) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=0, + ), + ] + ) + + initcode_subcontainer = Container( + name="Initcode Subcontainer", + sections=[ + Section.Code( + code=Op.RETURNCONTRACT[0](0, 0), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ), + Section.Container(container=runtime_container), + ], + ) + + assert factory_size == ( + len(initcode_subcontainer) - len(runtime_container) + ), "factory_size is wrong, expected factory_size is %d, calculated is %d" % ( + factory_size, + len(initcode_subcontainer), + ) + + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=initcode_subcontainer), + ] + ) + ), + } + # Storage in 0 should have the address, + # Storage 1 is a canary of 1 to make sure it tried to execute, which also covers cases of + # data+code being greater than initcode_size_max, which is allowed. + post = { + default_address: Account( + storage={ + slot_create_address: compute_eofcreate_address( + default_address, 0, initcode_subcontainer + ) + if target_deploy_size <= MAX_BYTECODE_SIZE + else value_create_failed, + slot_code_worked: value_code_worked, + } + ) + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +@pytest.mark.parametrize( + "auxdata_size", + [ + pytest.param(MAX_BYTECODE_SIZE - len(smallest_runtime_subcontainer), id="maxcode"), + pytest.param(MAX_BYTECODE_SIZE - len(smallest_runtime_subcontainer) + 1, id="overmaxcode"), + pytest.param(0x10000 - 60, id="almost64k"), + pytest.param(0x10000 - 1, id="64k-1"), + pytest.param(0x10000, id="64k"), + pytest.param(0x10000 + 1, id="over64k"), + ], +) +def test_auxdata_size_failures(state_test: StateTestFiller, auxdata_size: int): + """ + Exercises a number of auxdata size violations, and one maxcode success + """ + env = Environment() + auxdata_bytes = b"a" * auxdata_size + + initcode_subcontainer = Container( + name="Initcode Subcontainer", + sections=[ + Section.Code( + code=Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.RETURNCONTRACT[0](0, Op.CALLDATASIZE), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=3, + ), + Section.Container(container=smallest_runtime_subcontainer), + ], + ) + + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, Op.CALLDATASIZE)) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=initcode_subcontainer), + ] + ) + ), + } + + deployed_container_size = len(smallest_runtime_subcontainer) + auxdata_size + + # Storage in 0 will have address in first test, 0 in all other cases indicating failure + # Storage 1 in 1 is a canary to see if EOFCREATE opcode halted + post = { + default_address: Account( + storage={ + slot_create_address: compute_eofcreate_address( + default_address, 0, initcode_subcontainer + ) + if deployed_container_size <= MAX_BYTECODE_SIZE + else 0, + slot_code_worked: value_code_worked, + } + ) + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction(payload=auxdata_bytes)) + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(1, id="1_wei"), + pytest.param(10**9, id="1_gwei"), + ], +) +def test_eofcreate_insufficient_stipend( + state_test: StateTestFiller, + value: int, +): + """ + Exercises an EOFCREATE that fails because the calling account does not have enough ether to + pay the stipend + """ + env = Environment() + initcode_container = Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](value, 0, 0, 0)) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=smallest_initcode_subcontainer), + ] + ) + pre = { + TestAddress: Account(balance=10**11, nonce=1), + default_address: Account(balance=value - 1, code=initcode_container), + } + # create will fail but not trigger a halt, so canary at storage 1 should be set + # also validate target created contract fails + post = { + default_address: Account( + storage={ + slot_create_address: value_create_failed, + slot_code_worked: value_code_worked, + } + ), + compute_eofcreate_address(default_address, 0, initcode_container): Account.NONEXISTENT, + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction()) + + +def test_insufficient_initcode_gas( + state_test: StateTestFiller, +): + """ + Excercises an EOFCREATE when there is not enough gas for the initcode charge + """ + env = Environment() + + initcode_data = b"a" * 0x5000 + initcode_container = Container( + name="Large Initcode Subcontainer", + sections=[ + Section.Code( + code=Op.RETURNCONTRACT[0](0, 0), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ), + Section.Container(container=smallest_runtime_subcontainer), + Section.Data(data=initcode_data), + ], + ) + + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_code_should_fail, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=initcode_container), + ], + ), + storage={ + slot_create_address: value_canary_should_not_change, + slot_code_should_fail: value_canary_should_not_change, + }, + ), + } + # enough gas for everything but EVM opcodes and EIP-150 reserves + gas_limit = 21_000 + 32_000 + (len(initcode_data) + 31) // 32 * 6 + # out_of_gas is triggered, so canary won't set value + # also validate target created contract fails + post = { + default_address: Account( + storage={ + slot_create_address: value_canary_should_not_change, + slot_code_should_fail: value_canary_should_not_change, + }, + ), + compute_eofcreate_address(default_address, 0, initcode_container): Account.NONEXISTENT, + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction(gas_limit=gas_limit)) + + +def test_insufficient_gas_memory_expansion( + state_test: StateTestFiller, +): + """ + Excercises an EOFCREATE when the memory for auxdata has not been expanded but is requested + """ + env = Environment() + + auxdata_size = 0x5000 + initcode_container = Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, auxdata_size)) + + Op.SSTORE(slot_code_should_fail, slot_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=smallest_initcode_subcontainer), + ], + ) + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=initcode_container, + storage={ + slot_create_address: value_canary_should_not_change, + slot_code_should_fail: value_canary_should_not_change, + }, + ), + } + # enough gas for everything but EVM opcodes and EIP-150 reserves + initcode_container_words = (len(initcode_container) + 31) // 32 + auxdata_size_words = (auxdata_size + 31) // 32 + gas_limit = ( + 21_000 + + 32_000 + + initcode_container_words * 6 + + 3 * auxdata_size_words + + auxdata_size_words * auxdata_size_words // 512 + ) + # out_of_gas is triggered, so canary won't set value + # also validate target created contract fails + post = { + default_address: Account( + storage={ + slot_create_address: value_canary_should_not_change, + slot_code_should_fail: value_canary_should_not_change, + }, + ), + compute_eofcreate_address(default_address, 0, initcode_container): Account.NONEXISTENT, + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction(gas_limit=gas_limit)) + + +def test_insufficient_returncontract_auxdata_gas( + state_test: StateTestFiller, +): + """ + Excercises an EOFCREATE when there is not enough gas for the initcode charge + """ + env = Environment() + + auxdata_size = 0x5000 + initcode_container = Container( + name="Large Initcode Subcontainer", + sections=[ + Section.Code( + code=Op.RETURNCONTRACT[0](0, auxdata_size), + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ), + Section.Container(container=smallest_runtime_subcontainer), + ], + ) + + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Container( + sections=[ + Section.Code( + code=Op.SSTORE(slot_create_address, Op.EOFCREATE[0](0, 0, 0, 0)) + + Op.SSTORE(slot_code_should_fail, value_code_worked) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=4, + ), + Section.Container(container=initcode_container), + ], + ), + storage={ + slot_create_address: value_canary_should_not_change, + slot_code_should_fail: value_canary_should_not_change, + }, + ), + } + # enough gas for everything but EVM opcodes and EIP-150 reserves + initcode_container_words = (len(initcode_container) + 31) // 32 + auxdata_size_words = (auxdata_size + 31) // 32 + gas_limit = ( + 21_000 + + 32_000 + + initcode_container_words * 6 + + 3 * auxdata_size_words + + auxdata_size_words * auxdata_size_words // 512 + ) + # out_of_gas is triggered, so canary won't set value + # also validate target created contract fails + post = { + default_address: Account( + storage={ + slot_create_address: value_canary_should_not_change, + slot_code_should_fail: value_canary_should_not_change, + }, + ), + compute_eofcreate_address(default_address, 0, initcode_container): Account.NONEXISTENT, + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction(gas_limit=gas_limit)) diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_legacy_eof_creates.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_legacy_eof_creates.py new file mode 100644 index 0000000000..bf6c8d9bb4 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_legacy_eof_creates.py @@ -0,0 +1,130 @@ +""" +Test interactions between CREATE, CREATE2, and EOFCREATE +""" + +from typing import SupportsBytes + +import pytest + +from ethereum_test_tools import Account, Environment +from ethereum_test_tools import Initcode as LegacyInitcode +from ethereum_test_tools import StateTestFiller, TestAddress +from ethereum_test_tools.vm.opcode import Opcodes +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from .helpers import ( + default_address, + simple_transaction, + slot_code_worked, + slot_create_address, + smallest_initcode_subcontainer, + smallest_runtime_subcontainer, + value_code_worked, + value_create_failed, +) +from .spec import EOF_FORK_NAME + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7620.md" +REFERENCE_SPEC_VERSION = "52ddbcdddcf72dd72427c319f2beddeb468e1737" + +pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) + + +@pytest.mark.parametrize( + "legacy_create_opcode", + [ + pytest.param(Op.CREATE, id="CREATE"), + pytest.param(Op.CREATE2, id="CREATE2"), + ], +) +@pytest.mark.parametrize( + "deploy_code", + [ + pytest.param(smallest_initcode_subcontainer, id="deploy_eof_initcontainer"), + pytest.param(smallest_runtime_subcontainer, id="deploy_eof_container"), + ], +) +def test_cross_version_creates_fail( + state_test: StateTestFiller, + legacy_create_opcode: Opcodes, + deploy_code: SupportsBytes, +): + """ + Verifies that CREATE and CREATE2 cannot create EOF contracts + """ + env = Environment() + salt_param = [0] if legacy_create_opcode == Op.CREATE2 else [] + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account( + code=Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE( + slot_create_address, legacy_create_opcode(0, 0, Op.CALLDATASIZE, *salt_param) + ) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP + ), + } + # Storage in 0 should be empty as the create/create2 should fail, + # and 1 in 1 to show execution continued and did not halt + post = { + default_address: Account( + storage={ + slot_create_address: value_create_failed, + slot_code_worked: value_code_worked, + } + ) + } + + state_test( + env=env, + pre=pre, + post=post, + tx=simple_transaction(payload=bytes(deploy_code)), + ) + + +@pytest.mark.parametrize( + "legacy_create_opcode", + [ + pytest.param(Op.CREATE, id="CREATE"), + pytest.param(Op.CREATE2, id="CREATE2"), + ], +) +@pytest.mark.parametrize( + "deploy_code", + [ + pytest.param(smallest_initcode_subcontainer, id="deploy_eof_initcontainer"), + pytest.param(smallest_runtime_subcontainer, id="deploy_eof_container"), + ], +) +def test_legacy_initcode_eof_contract_fails( + state_test: StateTestFiller, + legacy_create_opcode: Opcodes, + deploy_code: SupportsBytes, +): + """ + Verifies that legacy initcode cannot create EOF + """ + env = Environment() + init_code = LegacyInitcode(deploy_code=deploy_code) + salt_param = [0] if legacy_create_opcode == Op.CREATE2 else [] + factory_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE(slot_create_address, legacy_create_opcode(0, 0, Op.CALLDATASIZE, *salt_param)) + + Op.SSTORE(slot_code_worked, value_code_worked) + ) + + pre = { + TestAddress: Account(balance=10**21, nonce=1), + default_address: Account(code=factory_code), + } + # Storage in 0 should be empty as the final CREATE filed + # and 1 in 1 to show execution continued and did not halt + post = { + default_address: Account( + storage={slot_create_address: value_create_failed, slot_code_worked: value_code_worked} + ) + } + + state_test(env=env, pre=pre, post=post, tx=simple_transaction(payload=bytes(init_code))) diff --git a/whitelist.txt b/whitelist.txt index addac64700..a64e4e4b23 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -10,6 +10,7 @@ api apis at5 AutoSection +auxdata balance base64 basefee @@ -282,6 +283,7 @@ reentrant repo repo's repos +returndata returndatacopy returndatasize returncontract @@ -442,6 +444,7 @@ runpytest runtest subclasses subcommand +subcontainer substring substrings tf @@ -610,6 +613,7 @@ call callcode return delegatecall +eofcreate extcall extdelegatecall staticcall