diff --git a/tests/prague/eip7692_eof_v1/eip7069_extcall/helpers.py b/tests/prague/eip7692_eof_v1/eip7069_extcall/helpers.py index 432294faca3..ae66efa018e 100644 --- a/tests/prague/eip7692_eof_v1/eip7069_extcall/helpers.py +++ b/tests/prague/eip7692_eof_v1/eip7069_extcall/helpers.py @@ -16,10 +16,6 @@ slot_call_status = next(_slot) slot_calldata_1 = next(_slot) slot_calldata_2 = next(_slot) -slot_cold_gas = next(_slot) -slot_warm_gas = next(_slot) -slot_oog_call_result = next(_slot) -slot_sanity_call_result = next(_slot) slot_last_slot = next(_slot) @@ -28,8 +24,6 @@ """Storage values for common testing fields""" value_code_worked = 0x2015 -value_call_legacy_abort = 0 -value_call_legacy_success = 1 """Memory and storage value for calldata""" value_calldata_1 = 0xC1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1 diff --git a/tests/prague/eip7692_eof_v1/eip7069_extcall/test_gas.py b/tests/prague/eip7692_eof_v1/eip7069_extcall/test_gas.py index 3e418eaa22f..43196df5606 100644 --- a/tests/prague/eip7692_eof_v1/eip7069_extcall/test_gas.py +++ b/tests/prague/eip7692_eof_v1/eip7069_extcall/test_gas.py @@ -5,21 +5,14 @@ import pytest -from ethereum_test_tools import Account, Alloc, Environment, StateTestFiller, Transaction +from ethereum_test_tools import Alloc, Environment, StateTestFiller from ethereum_test_tools.eof.v1 import Container from ethereum_test_tools.vm.opcode import Opcodes as Op -from ethereum_test_vm import Bytecode, EVMCodeType +from ethereum_test_types.helpers import cost_memory_bytes from .. import EOF_FORK_NAME +from ..gas_test import gas_test from . import REFERENCE_SPEC_GIT_PATH, REFERENCE_SPEC_VERSION -from .helpers import ( - slot_cold_gas, - slot_oog_call_result, - slot_sanity_call_result, - slot_warm_gas, - value_call_legacy_abort, - value_call_legacy_success, -) REFERENCE_SPEC_GIT_PATH = REFERENCE_SPEC_GIT_PATH REFERENCE_SPEC_VERSION = REFERENCE_SPEC_VERSION @@ -45,112 +38,6 @@ def state_env() -> Environment: return Environment() -def gas_test( - state_test: StateTestFiller, - env: Environment, - pre: Alloc, - setup_code: Bytecode, - subject_code: Bytecode, - tear_down_code: Bytecode, - cold_gas: int, - warm_gas: int | None = None, -): - """ - Creates a State Test to check the gas cost of a sequence of EOF code. - - `setup_code` and `tear_down_code` are called multiple times during the test, and MUST NOT have - any side-effects which persist across message calls, and in particular, any effects on the gas - usage of `subject_code`. - """ - if cold_gas <= 0: - raise ValueError(f"Target gas allocations (cold_gas) must be > 0, got {cold_gas}") - if warm_gas is None: - warm_gas = cold_gas - - sender = pre.fund_eoa() - - address_baseline = pre.deploy_contract(Container.Code(setup_code + tear_down_code)) - address_subject = pre.deploy_contract( - Container.Code(setup_code + subject_code + tear_down_code) - ) - # 2 times GAS, POP, CALL, 6 times PUSH1 - instructions charged for at every gas run - gas_single_gas_run = 2 * 2 + 2 + WARM_ACCOUNT_ACCESS_GAS + 6 * 3 - address_legacy_harness = pre.deploy_contract( - code=( - # warm subject and baseline without executing - (Op.BALANCE(address_subject) + Op.POP + Op.BALANCE(address_baseline) + Op.POP) - # Baseline gas run - + ( - Op.GAS - + Op.CALL(address=address_baseline, gas=Op.GAS) - + Op.POP - + Op.GAS - + Op.SWAP1 - + Op.SUB - ) - # cold gas run - + ( - Op.GAS - + Op.CALL(address=address_subject, gas=Op.GAS) - + Op.POP - + Op.GAS - + Op.SWAP1 - + Op.SUB - ) - # warm gas run - + ( - Op.GAS - + Op.CALL(address=address_subject, gas=Op.GAS) - + Op.POP - + Op.GAS - + Op.SWAP1 - + Op.SUB - ) - # Store warm gas: DUP3 is the gas of the baseline gas run - + (Op.DUP3 + Op.SWAP1 + Op.SUB + Op.PUSH2(slot_warm_gas) + Op.SSTORE) - # store cold gas: DUP2 is the gas of the baseline gas run - + (Op.DUP2 + Op.SWAP1 + Op.SUB + Op.PUSH2(slot_cold_gas) + Op.SSTORE) - # oog gas run: - # - DUP7 is the gas of the baseline gas run, after other CALL args were pushed - # - subtract the gas charged by the harness - # - add warm gas charged by the subject - # - subtract 1 to cause OOG exception - + Op.SSTORE( - slot_oog_call_result, - Op.CALL( - gas=Op.ADD(warm_gas - gas_single_gas_run - 1, Op.DUP7), - address=address_subject, - ), - ) - # sanity gas run: not subtracting 1 to see if enough gas makes the call succeed - + Op.SSTORE( - slot_sanity_call_result, - Op.CALL( - gas=Op.ADD(warm_gas - gas_single_gas_run, Op.DUP7), - address=address_subject, - ), - ) - + Op.STOP - ), - evm_code_type=EVMCodeType.LEGACY, # Needs to be legacy to use GAS opcode - ) - - post = { - address_legacy_harness: Account( - storage={ - slot_warm_gas: warm_gas, - slot_cold_gas: cold_gas, - slot_oog_call_result: value_call_legacy_abort, - slot_sanity_call_result: value_call_legacy_success, - }, - ), - } - - tx = Transaction(to=address_legacy_harness, gas_limit=env.gas_limit, sender=sender) - - state_test(env=env, pre=pre, tx=tx, post=post) - - @pytest.mark.parametrize( ["opcode", "pre_setup", "cold_gas", "warm_gas", "new_account"], [ @@ -221,13 +108,8 @@ def gas_test( ], ) @pytest.mark.parametrize( - ["mem_expansion_size", "mem_expansion_extra_gas"], - [ - pytest.param(0, 0, id="no_mem_expansion"), - pytest.param(1, 3, id="1byte_mem_expansion"), - pytest.param(32, 3, id="1word_mem_expansion"), - pytest.param(33, 6, id="33bytes_mem_expansion"), - ], + "mem_expansion_bytes", + [0, 1, 32, 33], ) def test_ext_calls_gas( state_test: StateTestFiller, @@ -238,8 +120,7 @@ def test_ext_calls_gas( cold_gas: int, warm_gas: int, new_account: bool, - mem_expansion_size: int, - mem_expansion_extra_gas: int, + mem_expansion_bytes: int, ): """Tests variations of EXT*CALL gas, both warm and cold, without and with mem expansions""" address_target = ( @@ -250,9 +131,12 @@ def test_ext_calls_gas( state_test, state_env, pre, - setup_code=pre_setup + Op.PUSH1(mem_expansion_size) + Op.PUSH0 + Op.PUSH20(address_target), + setup_code=pre_setup + + Op.PUSH1(mem_expansion_bytes) + + Op.PUSH0 + + Op.PUSH20(address_target), subject_code=opcode, tear_down_code=Op.STOP, - cold_gas=cold_gas + mem_expansion_extra_gas, - warm_gas=warm_gas + mem_expansion_extra_gas, + cold_gas=cold_gas + cost_memory_bytes(mem_expansion_bytes, 0), + warm_gas=warm_gas + cost_memory_bytes(mem_expansion_bytes, 0), ) diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/helpers.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/helpers.py index fd3a15f62b1..2d2751f3bcd 100644 --- a/tests/prague/eip7692_eof_v1/eip7620_eof_create/helpers.py +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/helpers.py @@ -18,6 +18,7 @@ slot_returndata_size = next(_slot) slot_max_depth = next(_slot) slot_call_or_create = next(_slot) +slot_counter = next(_slot) slot_last_slot = next(_slot) @@ -30,12 +31,7 @@ value_eof_call_result_reverted = 1 value_eof_call_result_failed = 2 -smallest_runtime_subcontainer = Container( - name="Runtime Subcontainer", - sections=[ - Section.Code(code=Op.STOP), - ], -) +smallest_runtime_subcontainer = Container.Code(code=Op.STOP, name="Runtime Subcontainer") smallest_initcode_subcontainer = Container( name="Initcode Subcontainer", @@ -44,5 +40,46 @@ Section.Container(container=smallest_runtime_subcontainer), ], ) +smallest_initcode_subcontainer_gas = 2 * 3 + +aborting_container = Container.Code(Op.INVALID, name="Aborting Container") +reverting_container = Container.Code(Op.REVERT(0, 0), name="Reverting Container") +expensively_reverting_container = Container.Code( + Op.SHA3(0, 32) + Op.REVERT(0, 0), name="Expensively Reverting Container" +) +expensively_reverting_container_gas = 2 * 3 + 30 + 3 + 6 + 2 * 3 +big_runtime_subcontainer = Container.Code(Op.NOOP * 10000 + Op.STOP, name="Big Subcontainer") + +bigger_initcode_subcontainer_gas = 3 + 4 + 2 * 3 +bigger_initcode_subcontainer = Container( + name="Bigger Initcode Subcontainer", + sections=[ + Section.Code( + code=Op.RJUMPI[len(Op.RETURNCONTRACT[0](0, 0))](1) + + Op.RETURNCONTRACT[0](0, 0) + + Op.RETURNCONTRACT[1](0, 0) + ), + Section.Container(container=smallest_runtime_subcontainer), + Section.Container(container=smallest_runtime_subcontainer), + ], +) -aborting_container = Container.Code(Op.INVALID) +data_runtime_container = smallest_runtime_subcontainer.copy() +data_runtime_container.sections.append(Section.Data("0x00")) + +data_initcode_subcontainer = Container( + name="Data Initcode Subcontainer", + sections=[ + Section.Code(code=Op.RETURNCONTRACT[0](0, 0)), + Section.Container(container=data_runtime_container), + ], +) + +data_appending_initcode_subcontainer = Container( + name="Data Appending Initcode Subcontainer", + sections=[ + Section.Code(code=Op.RETURNCONTRACT[0](0, 1)), + Section.Container(container=smallest_runtime_subcontainer), + ], +) +data_appending_initcode_subcontainer_gas = 2 * 3 + 3 diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_gas.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_gas.py new file mode 100644 index 00000000000..e536414a021 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_gas.py @@ -0,0 +1,162 @@ +""" +Test good and bad EOFCREATE cases +""" + +import pytest + +from ethereum_test_tools import Alloc, Environment, StateTestFiller, compute_eofcreate_address +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_types.helpers import cost_memory_bytes + +from .. import EOF_FORK_NAME +from ..gas_test import gas_test +from .helpers import ( + aborting_container, + big_runtime_subcontainer, + bigger_initcode_subcontainer, + bigger_initcode_subcontainer_gas, + data_appending_initcode_subcontainer, + data_appending_initcode_subcontainer_gas, + data_initcode_subcontainer, + data_runtime_container, + expensively_reverting_container, + expensively_reverting_container_gas, + reverting_container, + slot_counter, + smallest_initcode_subcontainer, + smallest_initcode_subcontainer_gas, + smallest_runtime_subcontainer, +) + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7620.md" +REFERENCE_SPEC_VERSION = "52ddbcdddcf72dd72427c319f2beddeb468e1737" + +pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) + +EOFCREATE_GAS = 32000 + + +def make_factory(initcode: Container): + """ + Wraps an initcontainer into a minimal runtime container + """ + return Container( + name="Factory Subcontainer", + sections=[ + Section.Code(Op.EOFCREATE[0](0, 0, 0, 0) + Op.STOP), + Section.Container(initcode), + ], + ) + + +@pytest.mark.parametrize("value", [0, 1]) +@pytest.mark.parametrize("new_account", [True, False]) +@pytest.mark.parametrize( + "mem_expansion_bytes", + [0, 1, 32, 33], +) +@pytest.mark.parametrize( + ["initcode", "initcode_execution_cost", "runtime"], + [ + pytest.param( + smallest_initcode_subcontainer, + smallest_initcode_subcontainer_gas, + smallest_runtime_subcontainer, + id="smallest_code", + ), + pytest.param( + Container.Init(aborting_container), + smallest_initcode_subcontainer_gas, + aborting_container, + id="aborting_runtime", + ), + pytest.param( + reverting_container, smallest_initcode_subcontainer_gas, None, id="reverting_initcode" + ), + pytest.param( + expensively_reverting_container, + expensively_reverting_container_gas, + None, + id="expensively_reverting_initcode", + ), + pytest.param( + Container.Init(big_runtime_subcontainer), + smallest_initcode_subcontainer_gas, + big_runtime_subcontainer, + id="big_runtime", + ), + pytest.param( + Container.Init(make_factory(smallest_initcode_subcontainer)), + smallest_initcode_subcontainer_gas, + make_factory(smallest_initcode_subcontainer), + id="nested_initcode", + ), + pytest.param( + bigger_initcode_subcontainer, + bigger_initcode_subcontainer_gas, + smallest_runtime_subcontainer, + id="bigger_initcode", + ), + pytest.param( + data_initcode_subcontainer, + smallest_initcode_subcontainer_gas, + data_runtime_container, + id="data_initcode", + ), + pytest.param( + data_appending_initcode_subcontainer, + data_appending_initcode_subcontainer_gas, + data_runtime_container, + id="data_appending_initcode", + ), + ], +) +def test_eofcreate_gas( + state_test: StateTestFiller, + pre: Alloc, + value: int, + new_account: bool, + mem_expansion_bytes: int, + initcode: Container, + initcode_execution_cost: int, + runtime: Container, +): + """Tests variations of EOFCREATE gas""" + initcode_hashing_cost = 6 * ((len(initcode) + 31) // 32) + deployed_code_cost = 200 * len(runtime) if runtime else 0 + + subject_address = pre.fund_eoa(0) + + salt_addresses = [compute_eofcreate_address(subject_address, i, initcode) for i in range(4)] + + if not new_account: + for a in salt_addresses: + pre.fund_address(a, 1) + + # Using `TLOAD` / `TSTORE` to work around warm/cold gas differences. We need a counter to pick + # a distinct salt on each `EOFCREATE` and avoid running into address conflicts. + code_increment_counter = ( + Op.TLOAD(slot_counter) + Op.DUP1 + Op.TSTORE(slot_counter, Op.PUSH1(1) + Op.ADD) + ) + + gas_test( + state_test, + Environment(), + pre, + setup_code=Op.PUSH1(mem_expansion_bytes) + + Op.PUSH0 + + code_increment_counter + + Op.PUSH32(value), + subject_code=Op.EOFCREATE[0], + tear_down_code=Op.STOP, + cold_gas=EOFCREATE_GAS + + cost_memory_bytes(mem_expansion_bytes, 0) + + initcode_hashing_cost + + initcode_execution_cost + + deployed_code_cost, + subject_subcontainer=initcode, + subject_balance=value * 4, + subject_address=subject_address, + oog_difference=initcode_execution_cost + deployed_code_cost + 1, + ) diff --git a/tests/prague/eip7692_eof_v1/gas_test.py b/tests/prague/eip7692_eof_v1/gas_test.py new file mode 100644 index 00000000000..3c10c14fad2 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/gas_test.py @@ -0,0 +1,143 @@ +""" +Utility to generate gas usage related state tests automatically. +""" + +import itertools + +from ethereum_test_base_types.base_types import Address +from ethereum_test_tools import Account, Alloc, Environment, StateTestFiller, Transaction +from ethereum_test_tools.eof.v1 import Container +from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_types.eof.v1 import Section +from ethereum_test_vm import Bytecode, EVMCodeType + +WARM_ACCOUNT_ACCESS_GAS = 100 + +"""Storage addresses for common testing fields""" +_slot = itertools.count() +slot_cold_gas = next(_slot) +slot_warm_gas = next(_slot) +slot_oog_call_result = next(_slot) +slot_sanity_call_result = next(_slot) +value_call_legacy_abort = 0 +value_call_legacy_success = 1 + + +def gas_test( + state_test: StateTestFiller, + env: Environment, + pre: Alloc, + setup_code: Bytecode, + subject_code: Bytecode, + tear_down_code: Bytecode, + cold_gas: int, + warm_gas: int | None = None, + subject_subcontainer: Container | None = None, + subject_address: Address | None = None, + subject_balance: int = 0, + oog_difference: int = 1, +): + """ + Creates a State Test to check the gas cost of a sequence of EOF code. + + `setup_code` and `tear_down_code` are called multiple times during the test, and MUST NOT have + any side-effects which persist across message calls, and in particular, any effects on the gas + usage of `subject_code`. + """ + if cold_gas <= 0: + raise ValueError(f"Target gas allocations (cold_gas) must be > 0, got {cold_gas}") + if warm_gas is None: + warm_gas = cold_gas + + sender = pre.fund_eoa() + + address_baseline = pre.deploy_contract(Container.Code(setup_code + tear_down_code)) + code_subject = setup_code + subject_code + tear_down_code + address_subject = pre.deploy_contract( + Container.Code(code_subject) + if not subject_subcontainer + else Container( + sections=[ + Section.Code(code_subject), + Section.Container(subject_subcontainer), + ] + ), + balance=subject_balance, + address=subject_address, + ) + # 2 times GAS, POP, CALL, 6 times PUSH1 - instructions charged for at every gas run + gas_single_gas_run = 2 * 2 + 2 + WARM_ACCOUNT_ACCESS_GAS + 6 * 3 + address_legacy_harness = pre.deploy_contract( + code=( + # warm subject and baseline without executing + (Op.BALANCE(address_subject) + Op.POP + Op.BALANCE(address_baseline) + Op.POP) + # Baseline gas run + + ( + Op.GAS + + Op.CALL(address=address_baseline, gas=Op.GAS) + + Op.POP + + Op.GAS + + Op.SWAP1 + + Op.SUB + ) + # cold gas run + + ( + Op.GAS + + Op.CALL(address=address_subject, gas=Op.GAS) + + Op.POP + + Op.GAS + + Op.SWAP1 + + Op.SUB + ) + # warm gas run + + ( + Op.GAS + + Op.CALL(address=address_subject, gas=Op.GAS) + + Op.POP + + Op.GAS + + Op.SWAP1 + + Op.SUB + ) + # Store warm gas: DUP3 is the gas of the baseline gas run + + (Op.DUP3 + Op.SWAP1 + Op.SUB + Op.PUSH2(slot_warm_gas) + Op.SSTORE) + # store cold gas: DUP2 is the gas of the baseline gas run + + (Op.DUP2 + Op.SWAP1 + Op.SUB + Op.PUSH2(slot_cold_gas) + Op.SSTORE) + # oog gas run: + # - DUP7 is the gas of the baseline gas run, after other CALL args were pushed + # - subtract the gas charged by the harness + # - add warm gas charged by the subject + # - subtract `oog_difference` to cause OOG exception (1 by default) + + Op.SSTORE( + slot_oog_call_result, + Op.CALL( + gas=Op.ADD(warm_gas - gas_single_gas_run - oog_difference, Op.DUP7), + address=address_subject, + ), + ) + # sanity gas run: not subtracting 1 to see if enough gas makes the call succeed + + Op.SSTORE( + slot_sanity_call_result, + Op.CALL( + gas=Op.ADD(warm_gas - gas_single_gas_run, Op.DUP7), + address=address_subject, + ), + ) + + Op.STOP + ), + evm_code_type=EVMCodeType.LEGACY, # Needs to be legacy to use GAS opcode + ) + + post = { + address_legacy_harness: Account( + storage={ + slot_warm_gas: warm_gas, + slot_cold_gas: cold_gas, + slot_oog_call_result: value_call_legacy_abort, + slot_sanity_call_result: value_call_legacy_success, + }, + ), + } + + tx = Transaction(to=address_legacy_harness, gas_limit=env.gas_limit, sender=sender) + + state_test(env=env, pre=pre, tx=tx, post=post)