From 47147002a32a1b14ba64a7630c50ac3ffc088552 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 12 Feb 2026 22:34:40 +0100 Subject: [PATCH] refactor(testing): Refactor `Initcode` to allign `gas_cost` behavior with `Bytecode` (#2201) * refactor(testing): Allign `Initcode.gas_cost` to return the expected value. * Refactor: small refactor * refactor(tests): Initcode usages * fix: review comments --- .../tools/tests/test_code.py | 35 +++++++++- .../tools/tools_code/generators.py | 69 ++++++++++++------- .../test_block_access_lists_eip7702.py | 3 +- .../test_tstorage_clear_after_tx.py | 4 +- .../test_tstorage_create_contexts.py | 6 +- .../eip3860_initcode/test_initcode.py | 37 +++------- 6 files changed, 91 insertions(+), 63 deletions(-) diff --git a/packages/testing/src/execution_testing/tools/tests/test_code.py b/packages/testing/src/execution_testing/tools/tests/test_code.py index b99a5c0797..370f385488 100644 --- a/packages/testing/src/execution_testing/tools/tests/test_code.py +++ b/packages/testing/src/execution_testing/tools/tests/test_code.py @@ -27,7 +27,7 @@ ) from execution_testing.specs import StateTest from execution_testing.test_types import Alloc, Environment, Transaction -from execution_testing.vm import Op +from execution_testing.vm import Bytecode, Op from ..tools_code import CalldataCase, Case, Conditional, Initcode, Switch @@ -184,6 +184,39 @@ def test_initcode(initcode: Initcode, bytecode: bytes) -> None: # noqa: D103 assert bytes(initcode) == bytecode +@pytest.mark.parametrize( + "initcode,reference", + [ + pytest.param( + Initcode(), + Initcode(), + id="empty-deployed-code", + ), + pytest.param( + # Both initcodes deploy code of the same size, but the execution + # cost of the deployed codes differ, make sure that `gas_cost` + # is not influenced by the deployed code's execution cost. + Initcode(deploy_code=Op.MSTORE(0, 0, new_memory_size=0)), + Initcode(deploy_code=Op.MSTORE(0xFF, 0, new_memory_size=0xFF)), + id="non-empty-deployed-code", + ), + ], +) +def test_initcode_gas_cost(initcode: Initcode, reference: Initcode) -> None: + """ + Test that the gas cost of the initcode is calculated correctly. + """ + assert initcode.gas_cost(Cancun) == reference.gas_cost(Cancun) + if initcode.deploy_code != reference.deploy_code: + initcode_deploy_code = initcode.deploy_code + assert isinstance(initcode_deploy_code, Bytecode) + reference_deploy_code = reference.deploy_code + assert isinstance(reference_deploy_code, Bytecode) + assert initcode_deploy_code.gas_cost( + Cancun + ) != reference_deploy_code.gas_cost(Cancun) + + @pytest.mark.parametrize( "conditional_bytecode,expected", [ diff --git a/packages/testing/src/execution_testing/tools/tools_code/generators.py b/packages/testing/src/execution_testing/tools/tools_code/generators.py index 5c1be40ed7..99434da8ce 100644 --- a/packages/testing/src/execution_testing/tools/tools_code/generators.py +++ b/packages/testing/src/execution_testing/tools/tools_code/generators.py @@ -6,7 +6,7 @@ from pydantic import Field from execution_testing.base_types import Address, Bytes -from execution_testing.forks import Fork, Frontier +from execution_testing.forks import Fork from execution_testing.test_types import EOA, Transaction from execution_testing.vm import Bytecode, ForkOpcodeInterface, Op @@ -25,44 +25,33 @@ class Initcode(Bytecode): EIP-3860 are *not* taken into account by any of these calculated costs. """ - deploy_code: SupportsBytes | Bytes + deploy_code: Bytes | Bytecode """ Bytecode to be deployed by the initcode. """ - execution_gas: int - """ - Gas cost of executing the initcode, without considering deployment gas - costs. - """ - deployment_gas: int - """ - Gas cost of deploying the cost, subtracted after initcode execution, - """ def __new__( cls, *, - deploy_code: SupportsBytes | Bytes | None = None, + deploy_code: Bytecode | SupportsBytes | None = None, initcode_length: int | None = None, initcode_prefix: Bytecode | None = None, padding_byte: int = 0x00, name: str = "", - fork: Fork = Frontier, ) -> Self: """ - Generate legacy initcode that inits a contract with the specified code. + Generate an initcode that returns a contract with the specified code. The initcode can be padded to a specified length for testing purposes. - - Gas costs are calculated using the fork's gas costs and memory - expansion formula. Defaults to Frontier if no fork is provided. """ if deploy_code is None: deploy_code = Bytecode() + elif not isinstance(deploy_code, Bytecode): + deploy_code = Bytes(deploy_code) if initcode_prefix is None: initcode_prefix = Bytecode() initcode = initcode_prefix - code_length = len(bytes(deploy_code)) + code_length = len(deploy_code) # PUSH2: length= initcode += Op.PUSH2(code_length) @@ -89,7 +78,7 @@ def __new__( ) # RETURN: offset=0, length - initcode += Op.RETURN + initcode += Op.RETURN(code_deposit_size=len(deploy_code)) initcode_plus_deploy_code = bytes(initcode) + bytes(deploy_code) padding_bytes = bytes() @@ -112,16 +101,48 @@ def __new__( pushed_stack_items=initcode.pushed_stack_items, max_stack_height=initcode.max_stack_height, min_stack_height=initcode.min_stack_height, + name=name, + opcode_list=initcode.opcode_list, ) - instance._name_ = name instance.deploy_code = deploy_code - instance.execution_gas = initcode.gas_cost(fork) - instance.deployment_gas = Op.RETURN( - code_deposit_size=len(bytes(instance.deploy_code)) - ).gas_cost(fork) return instance + def execution_gas( + self, + fork: Type[ForkOpcodeInterface], + *, + block_number: int = 0, + timestamp: int = 0, + ) -> int: + """ + Gas cost of executing the initcode, charged before the code + deposit fee. + """ + return self.gas_cost( + fork, + block_number=block_number, + timestamp=timestamp, + ) - self.deployment_gas( + fork, + block_number=block_number, + timestamp=timestamp, + ) + + def deployment_gas( + self, + fork: Type[ForkOpcodeInterface], + *, + block_number: int = 0, + timestamp: int = 0, + ) -> int: + """ + Gas cost of deploying the contract. + """ + return Op.RETURN(code_deposit_size=len(self.deploy_code)).gas_cost( + fork, block_number=block_number, timestamp=timestamp + ) + class CodeGasMeasure(Bytecode): """ diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py index 5449210dd8..8a9ef8bbae 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py @@ -1207,8 +1207,7 @@ def test_bal_7702_delegated_create( authorization_list_or_count=tx.authorization_list, ) + deployer_code.gas_cost(fork) - + init_code.execution_gas - + gsc.G_CODE_DEPOSIT_BYTE * len(deploy_code) + + init_code.gas_cost(fork) ) refund_counter = gsc.R_AUTHORIZATION_EXISTING_AUTHORITY diff --git a/tests/cancun/eip1153_tstore/test_tstorage_clear_after_tx.py b/tests/cancun/eip1153_tstore/test_tstorage_clear_after_tx.py index b81307b742..9c59480a50 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_clear_after_tx.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_clear_after_tx.py @@ -36,9 +36,7 @@ def test_tstore_clear_after_deployment_tx( init_code = Op.TSTORE(1, 1) deploy_code = Op.SSTORE(1, Op.TLOAD(1)) - code = Initcode( - deploy_code=deploy_code, initcode_prefix=init_code, fork=fork - ) + code = Initcode(deploy_code=deploy_code, initcode_prefix=init_code) sender = pre.fund_eoa() diff --git a/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py b/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py index 7abb7c373b..6161c2caa0 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py @@ -17,7 +17,6 @@ Transaction, compute_create_address, ) -from execution_testing.forks.helpers import Fork from . import CreateOpcodeParams, PytestParameterEnum from .spec import ref_spec_1153 @@ -165,12 +164,9 @@ def initcode( # noqa: D102 self, deploy_code: Bytecode, constructor_code: Bytecode, - fork: Fork, ) -> Initcode: return Initcode( - deploy_code=deploy_code, - initcode_prefix=constructor_code, - fork=fork, + deploy_code=deploy_code, initcode_prefix=constructor_code ) @pytest.fixture() diff --git a/tests/shanghai/eip3860_initcode/test_initcode.py b/tests/shanghai/eip3860_initcode/test_initcode.py index 7883400eeb..1f66635377 100644 --- a/tests/shanghai/eip3860_initcode/test_initcode.py +++ b/tests/shanghai/eip3860_initcode/test_initcode.py @@ -46,7 +46,6 @@ def initcode(fork: Fork, initcode_name: str) -> Initcode: if initcode_name == "max_size_ones": return Initcode( name=initcode_name, - fork=fork, deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, initcode_length=fork.max_initcode_size(), padding_byte=0x01, @@ -54,7 +53,6 @@ def initcode(fork: Fork, initcode_name: str) -> Initcode: elif initcode_name == "max_size_zeros": return Initcode( name=initcode_name, - fork=fork, deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, initcode_length=fork.max_initcode_size(), padding_byte=0x00, @@ -62,7 +60,6 @@ def initcode(fork: Fork, initcode_name: str) -> Initcode: elif initcode_name == "over_limit_ones": return Initcode( name=initcode_name, - fork=fork, deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, initcode_length=fork.max_initcode_size() + 1, padding_byte=0x01, @@ -70,7 +67,6 @@ def initcode(fork: Fork, initcode_name: str) -> Initcode: elif initcode_name == "over_limit_zeros": return Initcode( name=initcode_name, - fork=fork, deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, initcode_length=fork.max_initcode_size() + 1, padding_byte=0x00, @@ -78,7 +74,6 @@ def initcode(fork: Fork, initcode_name: str) -> Initcode: elif initcode_name == "32_bytes": return Initcode( name=initcode_name, - fork=fork, deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, initcode_length=32, padding_byte=0x00, @@ -86,7 +81,6 @@ def initcode(fork: Fork, initcode_name: str) -> Initcode: elif initcode_name == "33_bytes": return Initcode( name=initcode_name, - fork=fork, deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, initcode_length=33, padding_byte=0x00, @@ -94,7 +88,6 @@ def initcode(fork: Fork, initcode_name: str) -> Initcode: elif initcode_name == "max_size_minus_word": return Initcode( name=initcode_name, - fork=fork, deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, initcode_length=fork.max_initcode_size() - 32, padding_byte=0x00, @@ -102,22 +95,16 @@ def initcode(fork: Fork, initcode_name: str) -> Initcode: elif initcode_name == "max_size_minus_word_plus_byte": return Initcode( name=initcode_name, - fork=fork, deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, initcode_length=fork.max_initcode_size() - 32 + 1, padding_byte=0x00, ) - elif initcode_name == "empty": - ic = Initcode(name=initcode_name, fork=fork) - ic._bytes_ = bytes() - ic.deployment_gas = 0 - ic.execution_gas = 0 - return ic - elif initcode_name == "single_byte": - ic = Initcode(name=initcode_name, fork=fork) - ic._bytes_ = bytes(Op.STOP) - ic.deployment_gas = 0 - ic.execution_gas = 0 + elif initcode_name == "empty" or initcode_name == "single_byte": + ic_bytecode = Op.STOP if initcode_name == "single_byte" else Bytecode() + # We insist on using `Initcode` to preserve `initcode.deploy_code` + ic = Initcode(name=initcode_name) + ic._bytes_ = bytes(ic_bytecode) + ic.opcode_list = ic_bytecode.opcode_list return ic else: raise ValueError(f"Unknown initcode_name: {initcode_name}") @@ -284,16 +271,12 @@ def exact_intrinsic_gas( @pytest.fixture def exact_execution_gas( - self, exact_intrinsic_gas: int, initcode: Initcode + self, fork: Fork, exact_intrinsic_gas: int, initcode: Initcode ) -> int: """ Calculate total execution gas cost. """ - return ( - exact_intrinsic_gas - + initcode.deployment_gas - + initcode.execution_gas - ) + return exact_intrinsic_gas + initcode.gas_cost(fork) @pytest.fixture def tx_error(self, gas_test_case: str) -> TransactionException | None: @@ -593,9 +576,7 @@ def test_create_opcode_initcode( else: expected_gas_usage = contract_creation_gas_cost # The initcode is only executed if the length check succeeds - expected_gas_usage += initcode.execution_gas - # The code is only deployed if the length check succeeds - expected_gas_usage += initcode.deployment_gas + expected_gas_usage += initcode.gas_cost(fork) # CREATE2 hashing cost should only be deducted if the initcode # does not exceed the max length