From 1785a46a65236cb0074652b8eb54a75aeb9812f9 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Thu, 21 Aug 2025 11:19:48 +0000 Subject: [PATCH 01/23] chore(ci): update fill prepatched script. --- .github/scripts/fill_prepatched_tests.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/scripts/fill_prepatched_tests.sh b/.github/scripts/fill_prepatched_tests.sh index 75a0a3d0738..fc572b15d90 100755 --- a/.github/scripts/fill_prepatched_tests.sh +++ b/.github/scripts/fill_prepatched_tests.sh @@ -29,6 +29,11 @@ rm -f filloutput.log uv run fill $MODIFIED_DELETED_FILES --clean --until=$FILL_UNTIL --evm-bin evmone-t8n --block-gas-limit $BLOCK_GAS_LIMIT -m "state_test or blockchain_test" --output $BASE_TEST_PATH > >(tee -a filloutput.log) 2> >(tee -a filloutput.log >&2) +if grep -q "no tests ran" filloutput.log; then + echo "any_modified_fixtures=false" >> "$GITHUB_OUTPUT" + exit 0 +fi + if grep -q "FAILURES" filloutput.log; then echo "Error: failed to generate .py tests from before the PR." exit 1 From 674885a7027fd355203a28a0b50f3678f9baecd3 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 21 Jul 2025 14:27:21 +0800 Subject: [PATCH 02/23] refactor(eip7883): update vector input structure --- .../eip7883_modexp_gas_increase/conftest.py | 159 ++++++++++++------ .../eip7883_modexp_gas_increase/helpers.py | 52 ++++-- .../test_modexp_thresholds.py | 9 +- .../{ => vector}/vectors.json | 0 4 files changed, 146 insertions(+), 74 deletions(-) rename tests/osaka/eip7883_modexp_gas_increase/{ => vector}/vectors.json (100%) diff --git a/tests/osaka/eip7883_modexp_gas_increase/conftest.py b/tests/osaka/eip7883_modexp_gas_increase/conftest.py index c1e76c278f0..88582f2d14d 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/conftest.py +++ b/tests/osaka/eip7883_modexp_gas_increase/conftest.py @@ -8,7 +8,7 @@ from ethereum_test_tools import Account, Address, Alloc, Storage, Transaction from ethereum_test_tools.vm.opcode import Opcodes as Op -from .helpers import Vector +from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput from .spec import Spec, Spec7883 @@ -19,53 +19,95 @@ def call_opcode() -> Op: @pytest.fixture -def gas_measure_contract(pre: Alloc, call_opcode: Op, fork: Fork, vector: Vector) -> Address: +def call_contract_post_storage() -> Storage: + """ + Storage of the test contract after the transaction is executed. + Note: Fixture `call_contract_code` fills the actual expected storage values. + """ + return Storage() + + +@pytest.fixture +def gas_measure_contract( + pre: Alloc, + call_opcode: Op, + fork: Fork, + modexp_expected: bytes, + precompile_gas: int, + precompile_gas_modifier: int, + call_contract_post_storage: Storage, +) -> Address: """Deploys a contract that measures ModExp gas consumption.""" + assert call_opcode in [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL] + value = [0] if call_opcode in [Op.CALL, Op.CALLCODE] else [] + call_code = call_opcode( - address=Spec.MODEXP_ADDRESS, - value=0, - args_offset=0, - args_size=Op.CALLDATASIZE, + precompile_gas + precompile_gas_modifier, + Spec.MODEXP_ADDRESS, + *value, + 0, + Op.CALLDATASIZE(), + 0, + 0, ) + gas_costs = fork.gas_costs() extra_gas = ( gas_costs.G_WARM_ACCOUNT_ACCESS - + (gas_costs.G_VERY_LOW * (len(call_opcode.kwargs) - 2)) # type: ignore - + (gas_costs.G_BASE * 3) + + (gas_costs.G_VERY_LOW * (len(call_opcode.kwargs) - 1)) # type: ignore + + gas_costs.G_BASE # CALLDATASIZE + + gas_costs.G_BASE # GAS ) - measure_code = ( + + # Build the gas measurement contract code + # Stack operations: + # [gas_start] + # [gas_start, call_result] + # [gas_start, call_result, gas_end] + # [gas_start, gas_end, call_result] + call_result_measurement = Op.GAS + call_code + Op.GAS + Op.SWAP1 + + # Calculate gas consumed: gas_start - (gas_end + extra_gas) + # Stack Operation: + # [gas_start, gas_end] + # [gas_start, gas_end, extra_gas] + # [gas_start, gas_end + extra_gas] + # [gas_end + extra_gas, gas_start] + # [gas_consumed] + gas_calculation = Op.PUSH2[extra_gas] + Op.ADD + Op.SWAP1 + Op.SUB + + code = ( Op.CALLDATACOPY(dest_offset=0, offset=0, size=Op.CALLDATASIZE) - + Op.GAS # [gas_start] - + call_code # [gas_start, call_result] - + Op.GAS # [gas_start, call_result, gas_end] - + Op.SWAP1 # [gas_start, gas_end, call_result] - + Op.PUSH1[0] # [gas_start, gas_end, call_result, 0] - + Op.SSTORE # [gas_start, gas_end] - + Op.PUSH2[extra_gas] # [gas_start, gas_end, extra_gas] - + Op.ADD # [gas_start, gas_end + extra_gas] - + Op.SWAP1 # [gas_end + extra_gas, gas_start] - + Op.SUB # [gas_start - (gas_end + extra_gas)] - + Op.PUSH1[1] # [gas_start - (gas_end + extra_gas), 1] - + Op.SSTORE # [] + + Op.SSTORE(call_contract_post_storage.store_next(True), call_result_measurement) + + Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation) + + Op.SSTORE( + call_contract_post_storage.store_next(len(modexp_expected)), + Op.RETURNDATASIZE(), + ) + # + Op.RETURNDATACOPY(dest_offset=0, offset=0, size=Op.RETURNDATASIZE()) + # + Op.SSTORE(call_contract_post_storage.store_next( + # keccak256(Bytes(vector.expected))), Op.SHA3(0, Op.RETURNDATASIZE())) ) - measure_code += Op.SSTORE(2, Op.RETURNDATASIZE()) - for i in range(len(vector.expected) // 32): - measure_code += Op.RETURNDATACOPY(0, i * 32, 32) - measure_code += Op.SSTORE(i + 3, Op.MLOAD(0)) - measure_code += Op.STOP() - return pre.deploy_contract(measure_code) + for i in range(len(modexp_expected) // 32): + code += Op.RETURNDATACOPY(0, i * 32, 32) + code += Op.SSTORE( + call_contract_post_storage.store_next(modexp_expected[i * 32 : (i + 1) * 32]), + Op.MLOAD(0), + ) + + return pre.deploy_contract(code) @pytest.fixture -def precompile_gas(fork: Fork, vector: Vector) -> int: +def precompile_gas(fork: Fork, modexp_input: ModExpInput, gas_old: int, gas_new: int) -> int: """Calculate gas cost for the ModExp precompile and verify it matches expected gas.""" spec = Spec if fork < Osaka else Spec7883 - expected_gas = vector.gas_old if fork < Osaka else vector.gas_new + expected_gas = gas_old if fork < Osaka else gas_new calculated_gas = spec.calculate_gas_cost( - len(vector.input.base), - len(vector.input.modulus), - len(vector.input.exponent), - vector.input.exponent, + len(modexp_input.base), + len(modexp_input.modulus), + len(modexp_input.exponent), + modexp_input.exponent, ) assert calculated_gas == expected_gas, ( f"Calculated gas {calculated_gas} != Vector gas {expected_gas}\n" @@ -78,43 +120,52 @@ def precompile_gas(fork: Fork, vector: Vector) -> int: return calculated_gas +@pytest.fixture +def precompile_gas_modifier() -> int: + """Return the gas modifier for the ModExp precompile.""" + return 0 + + @pytest.fixture def tx( - fork: Fork, pre: Alloc, gas_measure_contract: Address, - vector: Vector, - precompile_gas: int, + modexp_input: ModExpInput, + tx_gas_limit: int, ) -> Transaction: """Transaction to measure gas consumption of the ModExp precompile.""" - intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas_cost = intrinsic_gas_cost_calc(calldata=vector.input) - memory_expansion_gas_calc = fork.memory_expansion_gas_calculator() - memory_expansion_gas = memory_expansion_gas_calc(new_bytes=len(bytes(vector.input))) - sstore_gas = fork.gas_costs().G_STORAGE_SET * (len(vector.expected) // 32) return Transaction( sender=pre.fund_eoa(), to=gas_measure_contract, - data=vector.input, - gas_limit=intrinsic_gas_cost + data=bytes(modexp_input), + gas_limit=tx_gas_limit, + ) + + +@pytest.fixture +def tx_gas_limit( + fork: Fork, modexp_expected: bytes, modexp_input: ModExpInput, precompile_gas: int +) -> int: + """Transaction gas limit used for the test (Can be overridden in the test).""" + intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator() + memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator() + sstore_gas = fork.gas_costs().G_STORAGE_SET * (len(modexp_expected) // 32) + extra_gas = 100_000 + return ( + extra_gas + + intrinsic_gas_cost_calculator(calldata=bytes(modexp_input)) + + memory_expansion_gas_calculator(new_bytes=len(bytes(modexp_input))) + precompile_gas - + memory_expansion_gas + sstore_gas - + 100_000, ) @pytest.fixture def post( gas_measure_contract: Address, - precompile_gas: int, - vector: Vector, + call_contract_post_storage: Storage, ) -> Dict[Address, Account]: """Return expected post state with gas consumption check.""" - storage = Storage() - storage[0] = 1 - storage[1] = precompile_gas - storage[2] = len(vector.expected) - for i in range(len(vector.expected) // 32): - storage[i + 3] = vector.expected[i * 32 : (i + 1) * 32] - return {gas_measure_contract: Account(storage=storage)} + return { + gas_measure_contract: Account(storage=call_contract_post_storage), + } diff --git a/tests/osaka/eip7883_modexp_gas_increase/helpers.py b/tests/osaka/eip7883_modexp_gas_increase/helpers.py index 08161fe60c4..4057b071b95 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/helpers.py +++ b/tests/osaka/eip7883_modexp_gas_increase/helpers.py @@ -1,40 +1,58 @@ """Helper functions for the EIP-7883 ModExp gas cost increase tests.""" -import json import os from typing import Annotated, List -from pydantic import BaseModel, Field, PlainValidator +import pytest +from pydantic import BaseModel, ConfigDict, Field, PlainValidator, RootModel, TypeAdapter +from pydantic.alias_generators import to_pascal from ethereum_test_tools import Bytes from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput +def current_python_script_directory(*args: str) -> str: + """Get the current Python script directory.""" + return os.path.join(os.path.dirname(os.path.realpath(__file__)), *args) + + class Vector(BaseModel): """A vector for the ModExp gas cost increase tests.""" - input: Annotated[ModExpInput, PlainValidator(ModExpInput.from_bytes)] = Field( + modexp_input: Annotated[ModExpInput, PlainValidator(ModExpInput.from_bytes)] = Field( ..., alias="Input" ) - expected: Bytes = Field(..., alias="Expected") + modexp_expected: Bytes = Field(..., alias="Expected") name: str = Field(..., alias="Name") gas_old: int | None = Field(..., alias="GasOld") gas_new: int | None = Field(..., alias="GasNew") - @staticmethod - def from_json(vector_json: dict) -> "Vector": - """Create a Vector from a JSON dictionary.""" - return Vector.model_validate(vector_json) + model_config = ConfigDict(alias_generator=to_pascal) - @staticmethod - def from_file(filename: str) -> List["Vector"]: - """Create a list of Vectors from a file.""" - with open(current_python_script_directory(filename), "r") as f: - vectors_json = json.load(f) - return [Vector.from_json(vector_json) for vector_json in vectors_json] + def to_pytest_param(self): + """Convert the test vector to a tuple that can be used as a parameter in a pytest test.""" + return pytest.param( + self.modexp_input, self.modexp_expected, self.gas_old, self.gas_new, id=self.name + ) -def current_python_script_directory(*args: str) -> str: - """Get the current Python script directory.""" - return os.path.join(os.path.dirname(os.path.realpath(__file__)), *args) +class VectorList(RootModel): + """A list of test vectors for the ModExp gas cost increase tests.""" + + root: List[Vector] + + +VectorListAdapter = TypeAdapter(VectorList) + + +def vectors_from_file(filename: str) -> List: + """Load test vectors from a file.""" + with open( + current_python_script_directory( + "vector", + filename, + ), + "rb", + ) as f: + return [v.to_pytest_param() for v in VectorListAdapter.validate_json(f.read()).root] diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index d78f0221212..575cc5834a3 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -13,7 +13,7 @@ Transaction, ) -from .helpers import Vector +from .helpers import vectors_from_file from .spec import ref_spec_7883 REFERENCE_SPEC_GIT_PATH = ref_spec_7883.git_path @@ -22,9 +22,12 @@ pytestmark = pytest.mark.valid_from("Prague") -@pytest.mark.parametrize("vector", Vector.from_file("vectors.json"), ids=lambda v: v.name) +@pytest.mark.parametrize( + "modexp_input,modexp_expected,gas_old,gas_new", + vectors_from_file("vectors.json"), + ids=lambda v: v.name, +) def test_vectors_from_file( - vector: Vector, state_test: StateTestFiller, pre: Alloc, tx: Transaction, diff --git a/tests/osaka/eip7883_modexp_gas_increase/vectors.json b/tests/osaka/eip7883_modexp_gas_increase/vector/vectors.json similarity index 100% rename from tests/osaka/eip7883_modexp_gas_increase/vectors.json rename to tests/osaka/eip7883_modexp_gas_increase/vector/vectors.json From d1566f9b3fe82d9629fa1b10b872eb460d9cfb74 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 21 Jul 2025 16:28:44 +0800 Subject: [PATCH 03/23] feat: add eip7702, gas usage and extra edge cases --- .../eip7883_modexp_gas_increase/conftest.py | 84 +++++++++++----- .../osaka/eip7883_modexp_gas_increase/spec.py | 13 +++ .../test_modexp_thresholds.py | 97 ++++++++++++++++++- .../eip7702_set_code_tx/test_set_code_txs.py | 2 +- 4 files changed, 168 insertions(+), 28 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/conftest.py b/tests/osaka/eip7883_modexp_gas_increase/conftest.py index 88582f2d14d..eaa14a0d114 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/conftest.py +++ b/tests/osaka/eip7883_modexp_gas_increase/conftest.py @@ -12,6 +12,18 @@ from .spec import Spec, Spec7883 +@pytest.fixture +def gas_old() -> int | None: + """Gas value from the test vector if any.""" + return None + + +@pytest.fixture +def gas_new() -> int | None: + """Gas value from the test vector if any.""" + return None + + @pytest.fixture def call_opcode() -> Op: """Return default call used to call the precompile.""" @@ -27,6 +39,15 @@ def call_contract_post_storage() -> Storage: return Storage() +@pytest.fixture +def call_succeeds() -> bool: + """ + By default, depending on the expected output, we can deduce if the call is expected to succeed + or fail. + """ + return True + + @pytest.fixture def gas_measure_contract( pre: Alloc, @@ -36,6 +57,7 @@ def gas_measure_contract( precompile_gas: int, precompile_gas_modifier: int, call_contract_post_storage: Storage, + call_succeeds: bool, ) -> Address: """Deploys a contract that measures ModExp gas consumption.""" assert call_opcode in [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL] @@ -78,46 +100,56 @@ def gas_measure_contract( code = ( Op.CALLDATACOPY(dest_offset=0, offset=0, size=Op.CALLDATASIZE) - + Op.SSTORE(call_contract_post_storage.store_next(True), call_result_measurement) - + Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation) + + Op.SSTORE(call_contract_post_storage.store_next(call_succeeds), call_result_measurement) + Op.SSTORE( call_contract_post_storage.store_next(len(modexp_expected)), Op.RETURNDATASIZE(), ) + ) + + if call_succeeds: + code += Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation) # + Op.RETURNDATACOPY(dest_offset=0, offset=0, size=Op.RETURNDATASIZE()) # + Op.SSTORE(call_contract_post_storage.store_next( # keccak256(Bytes(vector.expected))), Op.SHA3(0, Op.RETURNDATASIZE())) - ) - for i in range(len(modexp_expected) // 32): - code += Op.RETURNDATACOPY(0, i * 32, 32) - code += Op.SSTORE( - call_contract_post_storage.store_next(modexp_expected[i * 32 : (i + 1) * 32]), - Op.MLOAD(0), - ) + + for i in range(len(modexp_expected) // 32): + code += Op.RETURNDATACOPY(0, i * 32, 32) + code += Op.SSTORE( + call_contract_post_storage.store_next(modexp_expected[i * 32 : (i + 1) * 32]), + Op.MLOAD(0), + ) return pre.deploy_contract(code) @pytest.fixture -def precompile_gas(fork: Fork, modexp_input: ModExpInput, gas_old: int, gas_new: int) -> int: +def precompile_gas( + fork: Fork, modexp_input: ModExpInput, gas_old: int | None, gas_new: int | None +) -> int: """Calculate gas cost for the ModExp precompile and verify it matches expected gas.""" spec = Spec if fork < Osaka else Spec7883 - expected_gas = gas_old if fork < Osaka else gas_new - calculated_gas = spec.calculate_gas_cost( - len(modexp_input.base), - len(modexp_input.modulus), - len(modexp_input.exponent), - modexp_input.exponent, - ) - assert calculated_gas == expected_gas, ( - f"Calculated gas {calculated_gas} != Vector gas {expected_gas}\n" - f"Lengths: base: {hex(len(vector.input.base))} ({len(vector.input.base)}), " - f"exponent: {hex(len(vector.input.exponent))} ({len(vector.input.exponent)}), " - f"modulus: {hex(len(vector.input.modulus))} ({len(vector.input.modulus)})\n" - f"Exponent: {vector.input.exponent} " - f"({int.from_bytes(vector.input.exponent, byteorder='big')})" - ) - return calculated_gas + try: + calculated_gas = spec.calculate_gas_cost( + len(modexp_input.base), + len(modexp_input.modulus), + len(modexp_input.exponent), + modexp_input.exponent, + ) + if gas_old is not None and gas_new is not None: + expected_gas = gas_old if fork < Osaka else gas_new + assert calculated_gas == expected_gas, ( + f"Calculated gas {calculated_gas} != Vector gas {expected_gas}\n" + f"Lengths: base: {hex(len(vector.input.base))} ({len(vector.input.base)}), " + f"exponent: {hex(len(vector.input.exponent))} ({len(vector.input.exponent)}), " + f"modulus: {hex(len(vector.input.modulus))} ({len(vector.input.modulus)})\n" + f"Exponent: {vector.input.exponent} " + f"({int.from_bytes(vector.input.exponent, byteorder='big')})" + ) + return calculated_gas + except Exception as e: + print(f"Error calculating gas: {e}") + return 0 @pytest.fixture diff --git a/tests/osaka/eip7883_modexp_gas_increase/spec.py b/tests/osaka/eip7883_modexp_gas_increase/spec.py index ee36f17c709..8d11966235a 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/spec.py +++ b/tests/osaka/eip7883_modexp_gas_increase/spec.py @@ -2,6 +2,8 @@ from dataclasses import dataclass +from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput + @dataclass(frozen=True) class ReferenceSpec: @@ -37,6 +39,17 @@ class Spec: EXPONENT_THRESHOLD = 32 GAS_DIVISOR = 3 + # Test Constants + modexp_input = ModExpInput( + base="e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c0001020304050607", + exponent="01ffffff", + modulus="f01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c950001020304050607", + ) + modexp_expected = bytes.fromhex( + "1abce71dc2205cce4eb6934397a88136f94641342e283cbcd30e929e85605c6718ed67f475192ffd" + ) + modexp_error = bytes() + @classmethod def calculate_multiplication_complexity(cls, base_length: int, modulus_length: int) -> int: """Calculate the multiplication complexity of the ModExp precompile.""" diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index 575cc5834a3..4160b55568b 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -7,14 +7,16 @@ import pytest +from ethereum_test_checklists import EIPChecklist from ethereum_test_tools import ( Alloc, StateTestFiller, Transaction, ) +from ethereum_test_tools.vm.opcode import Opcodes as Op from .helpers import vectors_from_file -from .spec import ref_spec_7883 +from .spec import Spec, ref_spec_7883 REFERENCE_SPEC_GIT_PATH = ref_spec_7883.git_path REFERENCE_SPEC_VERSION = ref_spec_7883.version @@ -39,3 +41,96 @@ def test_vectors_from_file( tx=tx, post=post, ) + + +@pytest.mark.parametrize( + "modexp_input,modexp_expected,call_succeeds", + [ + pytest.param(bytes(), bytes(), False, id="zero-length-calldata"), + ], +) +@EIPChecklist.Precompile.Test.Inputs.AllZeros +def test_modexp_invalid( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, + post: Dict, +): + """Test ModExp gas cost using the test vectors from EIP-7883.""" + state_test( + pre=pre, + tx=tx, + post=post, + ) + + +@pytest.mark.parametrize( + "call_opcode", + [ + Op.CALL, + Op.STATICCALL, + Op.DELEGATECALL, + Op.CALLCODE, + ], +) +@pytest.mark.parametrize( + "modexp_input,modexp_expected", + [ + pytest.param(Spec.modexp_input, Spec.modexp_expected, id="base-heavy"), + ], +) +@EIPChecklist.Precompile.Test.CallContexts.Static +@EIPChecklist.Precompile.Test.CallContexts.Delegate +@EIPChecklist.Precompile.Test.CallContexts.Callcode +@EIPChecklist.Precompile.Test.CallContexts.Normal +def test_modexp_call_operations( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, + post: Dict, +): + """Test ModExp call related operations with EIP-7883.""" + state_test( + pre=pre, + tx=tx, + post=post, + ) + + +@pytest.mark.parametrize( + "modexp_input,modexp_expected,precompile_gas_modifier,call_succeeds", + [ + pytest.param( + Spec.modexp_input, + Spec.modexp_expected, + 1, + True, + id="extra_gas", + ), + pytest.param( + Spec.modexp_input, + Spec.modexp_expected, + 0, + True, + id="exact_gas", + ), + pytest.param( + Spec.modexp_input, + Spec.modexp_error, + -1, + False, + id="insufficient_gas", + ), + ], +) +@EIPChecklist.Precompile.Test.ValueTransfer.Fee.Over +@EIPChecklist.Precompile.Test.ValueTransfer.Fee.Exact +@EIPChecklist.Precompile.Test.ValueTransfer.Fee.Under +def test_modexp_gas_usage( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, + post: Dict, +): + """Test ModExp gas cost using the test vectors from EIP-7883.""" + state_test(pre=pre, tx=tx, post=post) diff --git a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py index 2e86fe94b3f..f79af5358a0 100644 --- a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py +++ b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py @@ -2556,7 +2556,7 @@ def test_set_code_to_log( @pytest.mark.with_all_call_opcodes @pytest.mark.with_all_precompiles -@pytest.mark.eip_checklist("precompile/test/call_contexts/set_code", eips=[7951]) +@pytest.mark.eip_checklist("precompile/test/call_contexts/set_code", eips=[7951, 7883]) def test_set_code_to_precompile( state_test: StateTestFiller, pre: Alloc, From a8640f86b06936f21b61daee62a4c2eda26b5b50 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 21 Jul 2025 18:03:41 +0800 Subject: [PATCH 04/23] feat: add fork transition test --- .../test_modexp_thresholds_transition.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py new file mode 100644 index 00000000000..a59d2a5b357 --- /dev/null +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py @@ -0,0 +1,86 @@ +"""Test ModExp gas cost transition from EIP-7883 before and after the Osaka hard fork.""" + +import pytest + +from ethereum_test_forks import Fork +from ethereum_test_tools import Account, Alloc, Block, BlockchainTestFiller, Transaction +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from .spec import Spec, ref_spec_7883 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7883.git_path +REFERENCE_SPEC_VERSION = ref_spec_7883.version + +pytestmark = pytest.mark.valid_at_transition_to("Osaka", subsequent_forks=True) + + +@pytest.mark.parametrize( + "modexp_input,modexp_expected,gas_old,gas_new", + [ + pytest.param(Spec.modexp_input, Spec.modexp_expected, 200, 500), # Should be 1200 + ], +) +def test_modexp_fork_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + gas_old: int, + gas_new: int, + tx_gas_limit: int, +): + """Test ModExp gas cost transition from EIP-7883 before and after the Osaka hard fork.""" + call_code = Op.CALL( + address=Spec.MODEXP_ADDRESS, + args_size=Op.CALLDATASIZE, + ) + + gas_costs = fork.gas_costs() + extra_gas = ( + gas_costs.G_WARM_ACCOUNT_ACCESS + + (gas_costs.G_VERY_LOW * (len(Op.CALL.kwargs) - 2)) # type: ignore + + (gas_costs.G_BASE * 3) + ) + code = ( + Op.CALLDATACOPY(dest_offset=0, offset=0, size=Op.CALLDATASIZE) + + Op.GAS # [gas_start] + + call_code # [gas_start, call_result] + + Op.GAS # [gas_start, call_result, gas_end] + + Op.SWAP1 # [gas_start, gas_end, call_result] + + Op.POP # [gas_start, gas_end] + + Op.PUSH2[extra_gas] # [gas_start, gas_end, extra_gas] + + Op.ADD # [gas_start, gas_end + extra_gas] + + Op.SWAP1 # [gas_end + extra_gas, gas_start] + + Op.SUB # [gas_start - (gas_end + extra_gas)] + + Op.TIMESTAMP # [gas_start - (gas_end + extra_gas), TIMESTAMP] + + Op.SSTORE # [] + ) + + senders = [pre.fund_eoa() for _ in range(3)] + contracts = [pre.deploy_contract(code) for _ in range(3)] + timestamps = [14_999, 15_000, 15_001] + gas_values = [gas_old, gas_new, gas_new] + + blocks = [ + Block( + timestamp=ts, + txs=[ + Transaction( + to=contract, + sender=sender, + gas_limit=tx_gas_limit, + ) + ], + ) + for ts, contract, sender in zip(timestamps, contracts, senders, strict=False) + ] + + post = { + contract: Account(storage={ts: gas}) + for contract, ts, gas in zip(contracts, timestamps, gas_values, strict=False) + } + + blockchain_test( + pre=pre, + blocks=blocks, + post=post, + ) From 7a092b246bb902d55eabf66c463560da240dd227 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Tue, 22 Jul 2025 16:06:14 +0800 Subject: [PATCH 05/23] test: add extra invalid cases --- .../test_modexp_thresholds.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index 4160b55568b..b6038a452eb 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -47,6 +47,56 @@ def test_vectors_from_file( "modexp_input,modexp_expected,call_succeeds", [ pytest.param(bytes(), bytes(), False, id="zero-length-calldata"), + pytest.param( + bytes.fromhex( + format(10, "032x") # Bsize + + format(11, "032x") # Esize + + format(12, "032x") # Msize + + "FF" * 9 # E + + "FF" * 11 # M + + "FF" * 12 # B + ), + bytes(), + False, + id="b-too-short", + ), + pytest.param( + bytes.fromhex( + format(10, "032x") # Bsize + + format(11, "032x") # Esize + + format(12, "032x") # Msize + + "FF" * 10 # E + + "FF" * 10 # M + + "FF" * 12 # B + ), + bytes(), + False, + id="m-too-short", + ), + pytest.param( + bytes.fromhex( + format(10, "032x") # Bsize + + format(11, "032x") # Esize + + format(12, "032x") # Msize + + "FF" * 10 # E + + "FF" * 11 # M + + "FF" * 11 # B + ), + bytes(), + False, + id="e-too-short", + ), + # Not sure if this is valid + pytest.param( + bytes.fromhex( + format(0, "032x") # Bsize + + format(0, "032x") # Esize + + format(0, "032x") # Msize + ), + bytes(), + False, + id="all-zeros", + ), ], ) @EIPChecklist.Precompile.Test.Inputs.AllZeros @@ -134,3 +184,46 @@ def test_modexp_gas_usage( ): """Test ModExp gas cost using the test vectors from EIP-7883.""" state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "modexp_input,modexp_expected,precompile_gas_modifier,call_succeeds", + [ + pytest.param( + Spec.modexp_input, + Spec.modexp_expected, + 1, + True, + id="extra_gas", + ), + pytest.param( + Spec.modexp_input, + Spec.modexp_expected, + 0, + True, + id="exact_gas", + ), + pytest.param( + Spec.modexp_input, + Spec.modexp_error, + -1, + False, + id="insufficient_gas", + ), + ], +) +def test_modexp_entry_points( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, + modexp_input: bytes, + tx_gas_limit: int, +): + """Test ModExp entry points.""" + tx = Transaction( + to=Spec.MODEXP_ADDRESS, + sender=pre.fund_eoa(), + data=bytes(modexp_input), + gas_limit=tx_gas_limit, + ) + state_test(pre=pre, tx=tx, post={}) From 6a66a1ee7b30712fe4938d97c769cb54b0e1e300 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Tue, 22 Jul 2025 23:07:55 +0800 Subject: [PATCH 06/23] refactor(tests): Improve fixture and test descriptions for clarity --- tests/osaka/eip7883_modexp_gas_increase/conftest.py | 8 ++++---- .../eip7883_modexp_gas_increase/test_modexp_thresholds.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/conftest.py b/tests/osaka/eip7883_modexp_gas_increase/conftest.py index eaa14a0d114..cb45f40e486 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/conftest.py +++ b/tests/osaka/eip7883_modexp_gas_increase/conftest.py @@ -14,19 +14,19 @@ @pytest.fixture def gas_old() -> int | None: - """Gas value from the test vector if any.""" + """Get old gas cost from the test vector if any.""" return None @pytest.fixture def gas_new() -> int | None: - """Gas value from the test vector if any.""" + """Get new gas cost from the test vector if any.""" return None @pytest.fixture def call_opcode() -> Op: - """Return default call used to call the precompile.""" + """Return call operationused to call the precompile.""" return Op.CALL @@ -59,7 +59,7 @@ def gas_measure_contract( call_contract_post_storage: Storage, call_succeeds: bool, ) -> Address: - """Deploys a contract that measures ModExp gas consumption.""" + """Deploys a contract that measures ModExp gas consumption and execution result.""" assert call_opcode in [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL] value = [0] if call_opcode in [Op.CALL, Op.CALLCODE] else [] diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index b6038a452eb..9ed25f27441 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -106,7 +106,7 @@ def test_modexp_invalid( tx: Transaction, post: Dict, ): - """Test ModExp gas cost using the test vectors from EIP-7883.""" + """Test ModExp gas cost with invalid inputs.""" state_test( pre=pre, tx=tx, @@ -182,7 +182,7 @@ def test_modexp_gas_usage( tx: Transaction, post: Dict, ): - """Test ModExp gas cost using the test vectors from EIP-7883.""" + """Test ModExp gas cost with different precompile gas modifiers.""" state_test(pre=pre, tx=tx, post=post) @@ -219,7 +219,7 @@ def test_modexp_entry_points( modexp_input: bytes, tx_gas_limit: int, ): - """Test ModExp entry points.""" + """Test ModExp entry points with different precompile gas modifiers.""" tx = Transaction( to=Spec.MODEXP_ADDRESS, sender=pre.fund_eoa(), From 0c02660740be74b7281c749118080d569f281bf4 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Wed, 23 Jul 2025 16:06:07 +0800 Subject: [PATCH 07/23] refactor(tests): add helper for invalid case --- .../test_modexp_thresholds.py | 81 +++++++++++-------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index 9ed25f27441..079338133ec 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -43,64 +43,75 @@ def test_vectors_from_file( ) -@pytest.mark.parametrize( - "modexp_input,modexp_expected,call_succeeds", - [ +def create_modexp_input( + bsize: int, esize: int, msize: int, e_data: str = "", m_data: str = "", b_data: str = "" +) -> bytes: + """ + Create ModExp input data with specified sizes and data. + + Args: + bsize: Base size in bytes + esize: Exponent size in bytes + msize: Modulus size in bytes + e_data: Exponent data (hex string, if not provided, will be padded with FF) + m_data: Modulus data (hex string, if not provided, will be padded with FF) + b_data: Base data (hex string, if not provided, will be padded with FF) + + Returns: + ModExp input as bytes + + """ + e_padded = "FF" * esize if not e_data else e_data + m_padded = "FF" * msize if not m_data else m_data + b_padded = "FF" * bsize if not b_data else b_data + + # Format sizes as 32-byte hex strings + bsize_hex = format(bsize, "032x") + esize_hex = format(esize, "032x") + msize_hex = format(msize, "032x") + + # Concatenate all parts + input_hex = bsize_hex + esize_hex + msize_hex + e_padded + m_padded + b_padded + return bytes.fromhex(input_hex) + + +def generate_invalid_inputs_cases(): + """Generate test cases for invalid ModExp inputs.""" + return [ pytest.param(bytes(), bytes(), False, id="zero-length-calldata"), pytest.param( - bytes.fromhex( - format(10, "032x") # Bsize - + format(11, "032x") # Esize - + format(12, "032x") # Msize - + "FF" * 9 # E - + "FF" * 11 # M - + "FF" * 12 # B - ), + create_modexp_input(10, 11, 12, b_data="FF" * 9), bytes(), False, id="b-too-short", ), pytest.param( - bytes.fromhex( - format(10, "032x") # Bsize - + format(11, "032x") # Esize - + format(12, "032x") # Msize - + "FF" * 10 # E - + "FF" * 10 # M - + "FF" * 12 # B - ), + create_modexp_input(10, 11, 12, m_data="FF" * 10), bytes(), False, id="m-too-short", ), pytest.param( - bytes.fromhex( - format(10, "032x") # Bsize - + format(11, "032x") # Esize - + format(12, "032x") # Msize - + "FF" * 10 # E - + "FF" * 11 # M - + "FF" * 11 # B - ), + create_modexp_input(10, 11, 12, e_data="FF" * 11), bytes(), False, id="e-too-short", ), - # Not sure if this is valid pytest.param( - bytes.fromhex( - format(0, "032x") # Bsize - + format(0, "032x") # Esize - + format(0, "032x") # Msize - ), + create_modexp_input(0, 0, 0), bytes(), False, id="all-zeros", ), - ], + ] + + +@pytest.mark.parametrize( + "modexp_input,modexp_expected,call_succeeds", + generate_invalid_inputs_cases(), ) @EIPChecklist.Precompile.Test.Inputs.AllZeros -def test_modexp_invalid( +def test_modexp_invalid_inputs( state_test: StateTestFiller, pre: Alloc, tx: Transaction, From 1e6ececce6f4c19f53fdc7a9a1931e86197690fc Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Wed, 23 Jul 2025 16:47:02 +0800 Subject: [PATCH 08/23] feat: add invalud boundary test cases --- .../eip7883_modexp_gas_increase/conftest.py | 9 +- .../osaka/eip7883_modexp_gas_increase/spec.py | 1 + .../test_modexp_thresholds.py | 88 +++++++++++++++++-- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/conftest.py b/tests/osaka/eip7883_modexp_gas_increase/conftest.py index cb45f40e486..b2fb51b0f28 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/conftest.py +++ b/tests/osaka/eip7883_modexp_gas_increase/conftest.py @@ -183,7 +183,8 @@ def tx_gas_limit( memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator() sstore_gas = fork.gas_costs().G_STORAGE_SET * (len(modexp_expected) // 32) extra_gas = 100_000 - return ( + + total_gas = ( extra_gas + intrinsic_gas_cost_calculator(calldata=bytes(modexp_input)) + memory_expansion_gas_calculator(new_bytes=len(bytes(modexp_input))) @@ -191,6 +192,12 @@ def tx_gas_limit( + sstore_gas ) + tx_gas_limit_cap = fork.transaction_gas_limit_cap() + + if tx_gas_limit_cap is not None: + return min(tx_gas_limit_cap, total_gas) + return total_gas + @pytest.fixture def post( diff --git a/tests/osaka/eip7883_modexp_gas_increase/spec.py b/tests/osaka/eip7883_modexp_gas_increase/spec.py index 8d11966235a..b0556743c01 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/spec.py +++ b/tests/osaka/eip7883_modexp_gas_increase/spec.py @@ -34,6 +34,7 @@ class Spec: LARGE_BASE_MODULUS_MULTIPLIER = 1 MAX_LENGTH_THRESHOLD = 32 EXPONENT_BYTE_MULTIPLIER = 8 + MAX_LENGTH_BYTES = 1024 WORD_SIZE = 8 EXPONENT_THRESHOLD = 32 diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index 079338133ec..2c7944a9ef1 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -15,6 +15,7 @@ ) from ethereum_test_tools.vm.opcode import Opcodes as Op +from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput from .helpers import vectors_from_file from .spec import Spec, ref_spec_7883 @@ -75,9 +76,9 @@ def create_modexp_input( return bytes.fromhex(input_hex) -def generate_invalid_inputs_cases(): - """Generate test cases for invalid ModExp inputs.""" - return [ +@pytest.mark.parametrize( + "modexp_input,modexp_expected,call_succeeds", + [ pytest.param(bytes(), bytes(), False, id="zero-length-calldata"), pytest.param( create_modexp_input(10, 11, 12, b_data="FF" * 9), @@ -103,21 +104,92 @@ def generate_invalid_inputs_cases(): False, id="all-zeros", ), - ] + ], +) +@EIPChecklist.Precompile.Test.Inputs.AllZeros +def test_modexp_invalid_inputs( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, + post: Dict, +): + """Test ModExp gas cost with invalid inputs.""" + state_test( + pre=pre, + tx=tx, + post=post, + ) + + +def create_boundary_modexp_case( + base: str = "FF", exponent: str = "FF", modulus: str = "FF", case_id: str = "" +): + """ + Create a single boundary ModExp test case. + + Args: + base: Base data (hex string) + exponent: Exponent data (hex string) + modulus: Modulus data (hex string) + case_id: Test case identifier + + Returns: + pytest.param for the test case + + """ + modexp_input = ModExpInput( + base=base, + exponent=exponent, + modulus=modulus, + ) + return pytest.param(modexp_input, Spec.modexp_error, False, id=case_id) @pytest.mark.parametrize( "modexp_input,modexp_expected,call_succeeds", - generate_invalid_inputs_cases(), + [ + create_boundary_modexp_case( + base="FF" * (Spec.MAX_LENGTH_BYTES + 1), case_id="base-too-long" + ), + create_boundary_modexp_case( + exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), case_id="exponent-too-long" + ), + create_boundary_modexp_case( + modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), case_id="modulus-too-long" + ), + create_boundary_modexp_case( + base="FF" * (Spec.MAX_LENGTH_BYTES + 1), + exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), + modulus="FF", + case_id="base-exponent-too-long", + ), + create_boundary_modexp_case( + base="FF" * (Spec.MAX_LENGTH_BYTES + 1), + exponent="FF", + modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), + case_id="base-modulus-too-long", + ), + create_boundary_modexp_case( + base="FF", + exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), + modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), + case_id="exponent-modulus-too-long", + ), + create_boundary_modexp_case( + base="FF" * (Spec.MAX_LENGTH_BYTES + 1), + exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), + modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), + case_id="all-too-long", + ), + ], ) -@EIPChecklist.Precompile.Test.Inputs.AllZeros -def test_modexp_invalid_inputs( +def test_modexp_boundary_inputs( state_test: StateTestFiller, pre: Alloc, tx: Transaction, post: Dict, ): - """Test ModExp gas cost with invalid inputs.""" + """Test ModExp boundary inputs.""" state_test( pre=pre, tx=tx, From 7c6cef8a2034ebc29c77be826b198fb5eb5c4863 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Wed, 30 Jul 2025 00:09:14 +0800 Subject: [PATCH 09/23] chore: update boundary input case --- .../test_modexp_thresholds.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index 2c7944a9ef1..2cfd2d7a182 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -22,7 +22,7 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_7883.git_path REFERENCE_SPEC_VERSION = ref_spec_7883.version -pytestmark = pytest.mark.valid_from("Prague") +pytestmark = pytest.mark.valid_from("Osaka") @pytest.mark.parametrize( @@ -157,30 +157,30 @@ def create_boundary_modexp_case( create_boundary_modexp_case( modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), case_id="modulus-too-long" ), - create_boundary_modexp_case( - base="FF" * (Spec.MAX_LENGTH_BYTES + 1), - exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), - modulus="FF", - case_id="base-exponent-too-long", - ), - create_boundary_modexp_case( - base="FF" * (Spec.MAX_LENGTH_BYTES + 1), - exponent="FF", - modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), - case_id="base-modulus-too-long", - ), - create_boundary_modexp_case( - base="FF", - exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), - modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), - case_id="exponent-modulus-too-long", - ), - create_boundary_modexp_case( - base="FF" * (Spec.MAX_LENGTH_BYTES + 1), - exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), - modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), - case_id="all-too-long", - ), + # create_boundary_modexp_case( + # base="FF" * (Spec.MAX_LENGTH_BYTES + 1), + # exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), + # modulus="FF", + # case_id="base-exponent-too-long", + # ), + # create_boundary_modexp_case( + # base="FF" * (Spec.MAX_LENGTH_BYTES + 1), + # exponent="FF", + # modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), + # case_id="base-modulus-too-long", + # ), + # create_boundary_modexp_case( + # base="FF", + # exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), + # modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), + # case_id="exponent-modulus-too-long", + # ), + # create_boundary_modexp_case( + # base="FF" * (Spec.MAX_LENGTH_BYTES + 1), + # exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), + # modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), + # case_id="all-too-long", + # ), ], ) def test_modexp_boundary_inputs( From e15e66c6cdafaad414820799348cee4a02ed4580 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Wed, 30 Jul 2025 12:36:39 +0800 Subject: [PATCH 10/23] refactor(tests): simplify boundary modexp test cases --- .../test_modexp_thresholds.py | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index 2cfd2d7a182..4bdc0e0e30b 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -157,30 +157,12 @@ def create_boundary_modexp_case( create_boundary_modexp_case( modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), case_id="modulus-too-long" ), - # create_boundary_modexp_case( - # base="FF" * (Spec.MAX_LENGTH_BYTES + 1), - # exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), - # modulus="FF", - # case_id="base-exponent-too-long", - # ), - # create_boundary_modexp_case( - # base="FF" * (Spec.MAX_LENGTH_BYTES + 1), - # exponent="FF", - # modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), - # case_id="base-modulus-too-long", - # ), - # create_boundary_modexp_case( - # base="FF", - # exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), - # modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), - # case_id="exponent-modulus-too-long", - # ), - # create_boundary_modexp_case( - # base="FF" * (Spec.MAX_LENGTH_BYTES + 1), - # exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), - # modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), - # case_id="all-too-long", - # ), + create_boundary_modexp_case( + base="FF" * (Spec.MAX_LENGTH_BYTES + 1), + exponent="FF", + modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), + case_id="base-modulus-too-long", + ), ], ) def test_modexp_boundary_inputs( From 418e90a292afeb26d4c77e8fe31a60c161718d5b Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Wed, 30 Jul 2025 17:05:09 +0800 Subject: [PATCH 11/23] fix(tests): update fork transition test --- .../test_modexp_thresholds_transition.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py index a59d2a5b357..a5a10c866a9 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py @@ -6,6 +6,7 @@ from ethereum_test_tools import Account, Alloc, Block, BlockchainTestFiller, Transaction from ethereum_test_tools.vm.opcode import Opcodes as Op +from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput from .spec import Spec, ref_spec_7883 REFERENCE_SPEC_GIT_PATH = ref_spec_7883.git_path @@ -17,7 +18,7 @@ @pytest.mark.parametrize( "modexp_input,modexp_expected,gas_old,gas_new", [ - pytest.param(Spec.modexp_input, Spec.modexp_expected, 200, 500), # Should be 1200 + pytest.param(Spec.modexp_input, Spec.modexp_expected, 200, 1200), ], ) def test_modexp_fork_transition( @@ -27,6 +28,7 @@ def test_modexp_fork_transition( gas_old: int, gas_new: int, tx_gas_limit: int, + modexp_input: ModExpInput, ): """Test ModExp gas cost transition from EIP-7883 before and after the Osaka hard fork.""" call_code = Op.CALL( @@ -66,6 +68,7 @@ def test_modexp_fork_transition( txs=[ Transaction( to=contract, + data=modexp_input, sender=sender, gas_limit=tx_gas_limit, ) From cf3f302e006df5a0cf0e0c6d7d93a586edafc66b Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Thu, 31 Jul 2025 15:09:23 +0800 Subject: [PATCH 12/23] feat(test): add gas formula egde cases --- .../test_modexp_thresholds.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index 4bdc0e0e30b..cb7db016ec1 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -292,3 +292,90 @@ def test_modexp_entry_points( gas_limit=tx_gas_limit, ) state_test(pre=pre, tx=tx, post={}) + + +def create_modexp_variable_gas_test_cases(): + """ + Create test cases for ModExp variable gas cost testing. + + Returns: + List of pytest.param objects for the test cases + + """ + # Test case definitions: (base, exponent, modulus, expected_result, test_id) + test_cases = [ + ("", "", "", "", "Z0"), + ("01" * 16, "00" * 16, "02" * 16, "00" * 15 + "01", "S0"), + ("01" * 16, "00" * 15 + "03", "02" * 16, "01" * 16, "S1"), + ("01" * 32, "FF" * 32, "02" * 32, "01" * 32, "S2"), + ("01" * 16, "00" * 40, "02" * 16, "00" * 15 + "01", "S3"), + ("01" * 16, "00" * 39 + "01", "02" * 16, "01" * 16, "S4"), + ("01" * 24, "00", "02" * 8, "00" * 7 + "01", "S5"), + ("01" * 8, "01", "02" * 24, "00" * 16 + "01" * 8, "S6"), + ("01" * 40, "00" * 16, "02" * 40, "00" * 39 + "01", "L0"), + ("01" * 40, "FF" * 32, "02" * 40, "01" * 40, "L1"), + ("01" * 40, "00" * 40, "02" * 40, "00" * 39 + "01", "L2"), + ("01" * 40, "00" * 39 + "01", "02" * 40, "01" * 40, "L3"), + ("01" * 48, "01", "02" * 16, "01" * 16, "L4"), + ("01" * 16, "00" * 40, "02" * 48, "00" * 47 + "01", "L5"), + ] + + # Gas calculation parameters: + # + # Please refer to EIP-7883 for details of each function in the gas calculation. + # Link: https://eips.ethereum.org/EIPS/eip-7883 + # + # - calculate_multiplication_complexity: + # - Comp: if max_length <= 32 bytes, it is Small (S), otherwise it is Large (L) + # - Rel (Length Relation): base < modulus (<), base = modulus (=), base > modulus (>) + # + # - calculate_iteration_count + # - Iter (Iteration Case): + # - A: exp≤32 and exp=0 + # - B: exp≤32 and exp≠0 + # - C: exp>32 and low256=0 + # - D: exp>32 and low256≠0 + # + # - calculate_gas_cost + # - Clamp: True if raw gas < 500 (clamped to 500), False if raw gas ≥ 500 (no clamping) + + # Test case coverage table: + # ┌─────┬──────┬─────┬──────┬───────┬─────────┬─────────────────────────────────────────────┐ + # │ ID │ Comp │ Rel │ Iter │ Clamp │ Gas │ Description │ + # ├─────┼──────┼─────┼──────┼───────┼─────────┼─────────────────────────────────────────────┤ + # │ Z0 │ - │ - │ - │ - │ 500 │ Zero case - empty inputs │ + # │ S0 │ S │ = │ A │ True │ 500 │ Small, equal, zero exponent, clamped │ + # │ S1 │ S │ = │ B │ True │ 500 │ Small, equal, small exp, clamped │ + # │ S2 │ S │ = │ B │ False │ 4080 │ Small, equal, large exp, unclamped │ + # │ S3 │ S │ = │ C │ False │ 2032 │ Small, equal, large exp+zero low256 │ + # │ S4 │ S │ = │ D │ False │ 2048 │ Small, equal, large exp+non-zero low256 │ + # │ S5 │ S │ > │ A │ True │ 500 │ Small, base>mod, zero exp, clamped │ + # │ S6 │ S │ < │ B │ True │ 500 │ Small, base │ B │ True │ 500 │ Large, base>mod, small exp, clamped │ + # │ L5 │ L │ < │ C │ False │ 9144 │ Large, base Date: Thu, 31 Jul 2025 15:45:18 +0800 Subject: [PATCH 13/23] test: add extra casefor modexp invalid input --- .../test_modexp_thresholds.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index cb7db016ec1..10a060ccb6b 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -77,33 +77,45 @@ def create_modexp_input( @pytest.mark.parametrize( - "modexp_input,modexp_expected,call_succeeds", + "modexp_input,", [ - pytest.param(bytes(), bytes(), False, id="zero-length-calldata"), + pytest.param(bytes(), id="zero-length-calldata"), pytest.param( create_modexp_input(10, 11, 12, b_data="FF" * 9), - bytes(), - False, id="b-too-short", ), pytest.param( create_modexp_input(10, 11, 12, m_data="FF" * 10), - bytes(), - False, id="m-too-short", ), pytest.param( create_modexp_input(10, 11, 12, e_data="FF" * 11), - bytes(), - False, id="e-too-short", ), pytest.param( create_modexp_input(0, 0, 0), - bytes(), - False, id="all-zeros", ), + # These invalid inputs are from EIP-7823. + # Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7823.md#analysis + pytest.param( + bytes.fromhex("9e5faafc"), + id="invalid-case-1", + ), + pytest.param( + bytes.fromhex("85474728"), + id="invalid-case-2", + ), + pytest.param( + bytes.fromhex("9e281a98" + "00" * 54 + "021e19e0c9bab2400000"), + id="invalid-case-3", + ), + ], +) +@pytest.mark.parametrize( + "modexp_expected,call_succeeds", + [ + pytest.param(bytes(), False), ], ) @EIPChecklist.Precompile.Test.Inputs.AllZeros From 4bbcd5d6010485a7ae52131eaece04ae7eb7162b Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 4 Aug 2025 15:42:07 +0800 Subject: [PATCH 14/23] tests: port legacy modexp test --- .../eip198_modexp_precompile/helpers.py | 2 + .../eip7883_modexp_gas_increase/helpers.py | 4 +- .../test_modexp_thresholds.py | 19 ++ .../vector/legacy.json | 162 ++++++++++++++++++ 4 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json diff --git a/tests/byzantium/eip198_modexp_precompile/helpers.py b/tests/byzantium/eip198_modexp_precompile/helpers.py index 78fdc56fbad..b91341ae7f6 100644 --- a/tests/byzantium/eip198_modexp_precompile/helpers.py +++ b/tests/byzantium/eip198_modexp_precompile/helpers.py @@ -84,6 +84,8 @@ def from_bytes(cls, input_data: Bytes | str) -> "ModExpInput": modulus = padded_input_data[current_index : current_index + modulus_length] + modulus = modulus.ljust(min(1024, modulus_length), b"\x00") + return cls(base=base, exponent=exponent, modulus=modulus, raw_input=input_data) diff --git a/tests/osaka/eip7883_modexp_gas_increase/helpers.py b/tests/osaka/eip7883_modexp_gas_increase/helpers.py index 4057b071b95..62819fea4be 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/helpers.py +++ b/tests/osaka/eip7883_modexp_gas_increase/helpers.py @@ -25,8 +25,8 @@ class Vector(BaseModel): ) modexp_expected: Bytes = Field(..., alias="Expected") name: str = Field(..., alias="Name") - gas_old: int | None = Field(..., alias="GasOld") - gas_new: int | None = Field(..., alias="GasNew") + gas_old: int | None = Field(default=None, alias="GasOld") + gas_new: int | None = Field(default=None, alias="GasNew") model_config = ConfigDict(alias_generator=to_pascal) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index 10a060ccb6b..77ba792fbf1 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -44,6 +44,25 @@ def test_vectors_from_file( ) +@pytest.mark.parametrize( + "modexp_input,modexp_expected,gas_old,gas_new", + vectors_from_file("legacy.json"), + ids=lambda v: v.name, +) +def test_vectors_from_legacy_tests( + state_test: StateTestFiller, + pre: Alloc, + tx: Transaction, + post: Dict, +): + """Test ModExp gas cost using the test vectors from legacy tests.""" + state_test( + pre=pre, + tx=tx, + post=post, + ) + + def create_modexp_input( bsize: int, esize: int, msize: int, e_data: str = "", m_data: str = "", b_data: str = "" ) -> bytes: diff --git a/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json b/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json new file mode 100644 index 00000000000..3e5ef1045c1 --- /dev/null +++ b/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json @@ -0,0 +1,162 @@ +[ + { + "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002003fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2efffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + "Expected": "0000000000000000000000000000000000000000000000000000000000000001", + "Name": "legacy-case-0" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2efffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + "Expected": "0000000000000000000000000000000000000000000000000000000000000000", + "Name": "legacy-case-1" + }, + { + "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002003ffff800000000000000000000000000000000000000000000000000000000000000007", + "Expected": "3b01b01ac41f2d6e917c6d6a221ce793802469026d9ab7578fa2e79e4da6aaab", + "Name": "legacy-case-3" + }, + { + "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002003ffff80", + "Expected": "3b01b01ac41f2d6e917c6d6a221ce793802469026d9ab7578fa2e79e4da6aaab", + "Name": "legacy-case-4" + }, + { + "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002003", + "Expected": "0000000000000000000000000000000000000000000000000000000000000000", + "Name": "legacy-case-5" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020038000000000000000000000000000000000000000000000000000000000000000", + "Expected": "0000000000000000000000000000000000000000000000000000000000000001", + "Name": "legacy-case-6" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000080", + "Expected": "0000000000000000000000000000000000000000000000000000000000000001", + "Name": "legacy-case-7" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000", + "Expected": "0000000000000000000000000000000000000000000000000000000000000000", + "Name": "legacy-case-8" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000101", + "Expected": "", + "Name": "legacy-case-9" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000304", + "Expected": "00", + "Name": "legacy-case-10" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001020004", + "Expected": "01", + "Name": "legacy-case-11" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001020300", + "Expected": "00", + "Name": "legacy-case-12" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010304", + "Expected": "00", + "Name": "legacy-case-13" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010204", + "Expected": "01", + "Name": "legacy-case-14" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000203", + "Expected": "", + "Name": "legacy-case-15" + }, + { + "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000202030006", + "Expected": "0200", + "Name": "legacy-case-16" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001020306", + "Expected": "02", + "Name": "legacy-case-17" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002020300", + "Expected": "0000", + "Name": "legacy-case-18" + }, + { + "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000202030000", + "Expected": "0000", + "Name": "legacy-case-19" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020203", + "Expected": "0000", + "Name": "legacy-case-20" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002023003", + "Expected": "0000", + "Name": "legacy-case-21" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020230", + "Expected": "0000", + "Name": "legacy-case-22" + }, + { + "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000202", + "Expected": "0000", + "Name": "legacy-case-23" + }, + { + "Input": "0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002", + "Expected": "0000", + "Name": "legacy-case-24" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001001001010010", + "Expected": "01", + "Name": "legacy-case-25" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000064", + "Expected": "01", + "Name": "legacy-case-26" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010100000000000000000000000000000000000000000000000000000000000000020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030006", + "Expected": "0200", + "Name": "legacy-case-27" + }, + { + "Input": "000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010035ee4e488f45e64d2f07becd54646357381d32f30b74c299a8c25d5202c04938ef6c4764a04f10fc908b78c4486886000f6d290251a79681a83b950c7e5c373510", + "Expected": "01", + "Name": "legacy-case-31" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000cd935b43e42204fcbfb734a6e27735e8e90204fcc1fd2727bb040f9eecb", + "Expected": "010000000000000000000000", + "Name": "legacy-case-32" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000060846813a8d2d451387340fa0597c6545ae63", + "Expected": "010000000000", + "Name": "legacy-case-33" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000d02534f82b1013f20d9c7d18d62cd95674d2e013f20d9c7d18d62cd95674d2f", + "Expected": "01000000000000000000000000", + "Name": "legacy-case-34" + }, + { + "Input": "0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000120785e45de3d6be050ba3c4d33ff0bb2d010ace3b1dfe9c49f4c7a8075102fa19a86c010ace3b1dfe9c49f4c7a8075102fa19a86d", + "Expected": "000000010000000000000000000000000000", + "Name": "legacy-case-35" + } +] \ No newline at end of file From 5d608738bf456d345bd6e39ce703ebf9e881bf2e Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Thu, 7 Aug 2025 15:14:38 +0800 Subject: [PATCH 15/23] refactor: update vector and data types --- tests/byzantium/eip198_modexp_precompile/helpers.py | 2 -- .../eip7883_modexp_gas_increase/test_modexp_thresholds.py | 6 +----- tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json | 4 ++-- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/byzantium/eip198_modexp_precompile/helpers.py b/tests/byzantium/eip198_modexp_precompile/helpers.py index b91341ae7f6..78fdc56fbad 100644 --- a/tests/byzantium/eip198_modexp_precompile/helpers.py +++ b/tests/byzantium/eip198_modexp_precompile/helpers.py @@ -84,8 +84,6 @@ def from_bytes(cls, input_data: Bytes | str) -> "ModExpInput": modulus = padded_input_data[current_index : current_index + modulus_length] - modulus = modulus.ljust(min(1024, modulus_length), b"\x00") - return cls(base=base, exponent=exponent, modulus=modulus, raw_input=input_data) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index 77ba792fbf1..08ae6ff7287 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -8,11 +8,7 @@ import pytest from ethereum_test_checklists import EIPChecklist -from ethereum_test_tools import ( - Alloc, - StateTestFiller, - Transaction, -) +from ethereum_test_tools import Alloc, StateTestFiller, Transaction from ethereum_test_tools.vm.opcode import Opcodes as Op from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput diff --git a/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json b/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json index 3e5ef1045c1..9945496f873 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json +++ b/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json @@ -135,8 +135,8 @@ "Name": "legacy-case-27" }, { - "Input": "000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010035ee4e488f45e64d2f07becd54646357381d32f30b74c299a8c25d5202c04938ef6c4764a04f10fc908b78c4486886000f6d290251a79681a83b950c7e5c373510", - "Expected": "01", + "Input": "000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010035ee4e488f45e64d2f07becd54646357381d32f30b74c299a8c25d5202c04938ef6c4764a04f10fc908b78c4486886000f6d290251a79681a83b950c7e5c37351", + "Expected": "01000000000000000000000000000000", "Name": "legacy-case-31" }, { From e22e1f8d13298531213e994659c9cbad6f912698 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 11 Aug 2025 17:49:42 +0800 Subject: [PATCH 16/23] refactor: remove valid case from invalid scenario --- .../eip7883_modexp_gas_increase/conftest.py | 5 -- .../test_modexp_thresholds.py | 49 ------------------- 2 files changed, 54 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/conftest.py b/tests/osaka/eip7883_modexp_gas_increase/conftest.py index b2fb51b0f28..919242bdb6c 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/conftest.py +++ b/tests/osaka/eip7883_modexp_gas_increase/conftest.py @@ -140,11 +140,6 @@ def precompile_gas( expected_gas = gas_old if fork < Osaka else gas_new assert calculated_gas == expected_gas, ( f"Calculated gas {calculated_gas} != Vector gas {expected_gas}\n" - f"Lengths: base: {hex(len(vector.input.base))} ({len(vector.input.base)}), " - f"exponent: {hex(len(vector.input.exponent))} ({len(vector.input.exponent)}), " - f"modulus: {hex(len(vector.input.modulus))} ({len(vector.input.modulus)})\n" - f"Exponent: {vector.input.exponent} " - f"({int.from_bytes(vector.input.exponent, byteorder='big')})" ) return calculated_gas except Exception as e: diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index 08ae6ff7287..d1b88f5a81b 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -59,58 +59,9 @@ def test_vectors_from_legacy_tests( ) -def create_modexp_input( - bsize: int, esize: int, msize: int, e_data: str = "", m_data: str = "", b_data: str = "" -) -> bytes: - """ - Create ModExp input data with specified sizes and data. - - Args: - bsize: Base size in bytes - esize: Exponent size in bytes - msize: Modulus size in bytes - e_data: Exponent data (hex string, if not provided, will be padded with FF) - m_data: Modulus data (hex string, if not provided, will be padded with FF) - b_data: Base data (hex string, if not provided, will be padded with FF) - - Returns: - ModExp input as bytes - - """ - e_padded = "FF" * esize if not e_data else e_data - m_padded = "FF" * msize if not m_data else m_data - b_padded = "FF" * bsize if not b_data else b_data - - # Format sizes as 32-byte hex strings - bsize_hex = format(bsize, "032x") - esize_hex = format(esize, "032x") - msize_hex = format(msize, "032x") - - # Concatenate all parts - input_hex = bsize_hex + esize_hex + msize_hex + e_padded + m_padded + b_padded - return bytes.fromhex(input_hex) - - @pytest.mark.parametrize( "modexp_input,", [ - pytest.param(bytes(), id="zero-length-calldata"), - pytest.param( - create_modexp_input(10, 11, 12, b_data="FF" * 9), - id="b-too-short", - ), - pytest.param( - create_modexp_input(10, 11, 12, m_data="FF" * 10), - id="m-too-short", - ), - pytest.param( - create_modexp_input(10, 11, 12, e_data="FF" * 11), - id="e-too-short", - ), - pytest.param( - create_modexp_input(0, 0, 0), - id="all-zeros", - ), # These invalid inputs are from EIP-7823. # Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7823.md#analysis pytest.param( From 6f084479398d53cbc22f4e22a59e716cc3b342a8 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Tue, 12 Aug 2025 15:47:58 +0800 Subject: [PATCH 17/23] refactor: update result comparison method and test case --- .../eip7883_modexp_gas_increase/conftest.py | 18 ++++++------------ .../vector/legacy.json | 14 +++++++------- .../vector/vectors.json | 6 +++--- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/conftest.py b/tests/osaka/eip7883_modexp_gas_increase/conftest.py index 919242bdb6c..e171e3ee6d6 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/conftest.py +++ b/tests/osaka/eip7883_modexp_gas_increase/conftest.py @@ -5,7 +5,7 @@ import pytest from ethereum_test_forks import Fork, Osaka -from ethereum_test_tools import Account, Address, Alloc, Storage, Transaction +from ethereum_test_tools import Account, Address, Alloc, Bytes, Storage, Transaction, keccak256 from ethereum_test_tools.vm.opcode import Opcodes as Op from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput @@ -109,17 +109,11 @@ def gas_measure_contract( if call_succeeds: code += Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation) - # + Op.RETURNDATACOPY(dest_offset=0, offset=0, size=Op.RETURNDATASIZE()) - # + Op.SSTORE(call_contract_post_storage.store_next( - # keccak256(Bytes(vector.expected))), Op.SHA3(0, Op.RETURNDATASIZE())) - - for i in range(len(modexp_expected) // 32): - code += Op.RETURNDATACOPY(0, i * 32, 32) - code += Op.SSTORE( - call_contract_post_storage.store_next(modexp_expected[i * 32 : (i + 1) * 32]), - Op.MLOAD(0), - ) - + code += Op.RETURNDATACOPY(dest_offset=0, offset=0, size=Op.RETURNDATASIZE()) + code += Op.SSTORE( + call_contract_post_storage.store_next(keccak256(Bytes(modexp_expected))), + Op.SHA3(0, Op.RETURNDATASIZE()), + ) return pre.deploy_contract(code) diff --git a/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json b/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json index 9945496f873..882bd69d064 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json +++ b/tests/osaka/eip7883_modexp_gas_increase/vector/legacy.json @@ -76,7 +76,7 @@ }, { "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000202030006", - "Expected": "0200", + "Expected": "0002", "Name": "legacy-case-16" }, { @@ -131,32 +131,32 @@ }, { "Input": "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010100000000000000000000000000000000000000000000000000000000000000020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030006", - "Expected": "0200", + "Expected": "0002", "Name": "legacy-case-27" }, { "Input": "000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010035ee4e488f45e64d2f07becd54646357381d32f30b74c299a8c25d5202c04938ef6c4764a04f10fc908b78c4486886000f6d290251a79681a83b950c7e5c37351", - "Expected": "01000000000000000000000000000000", + "Expected": "00000000000000000000000000000001", "Name": "legacy-case-31" }, { "Input": "0000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000cd935b43e42204fcbfb734a6e27735e8e90204fcc1fd2727bb040f9eecb", - "Expected": "010000000000000000000000", + "Expected": "000000000000000000000001", "Name": "legacy-case-32" }, { "Input": "0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000060846813a8d2d451387340fa0597c6545ae63", - "Expected": "010000000000", + "Expected": "000000000001", "Name": "legacy-case-33" }, { "Input": "0000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000d02534f82b1013f20d9c7d18d62cd95674d2e013f20d9c7d18d62cd95674d2f", - "Expected": "01000000000000000000000000", + "Expected": "00000000000000000000000001", "Name": "legacy-case-34" }, { "Input": "0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000120785e45de3d6be050ba3c4d33ff0bb2d010ace3b1dfe9c49f4c7a8075102fa19a86c010ace3b1dfe9c49f4c7a8075102fa19a86d", - "Expected": "000000010000000000000000000000000000", + "Expected": "000000000000000000000000000000000001", "Name": "legacy-case-35" } ] \ No newline at end of file diff --git a/tests/osaka/eip7883_modexp_gas_increase/vector/vectors.json b/tests/osaka/eip7883_modexp_gas_increase/vector/vectors.json index 2e6cae64b76..bf1906cd04b 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/vector/vectors.json +++ b/tests/osaka/eip7883_modexp_gas_increase/vector/vectors.json @@ -1,7 +1,7 @@ [ { - "Input": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002", - "Expected": "000000000000000000000000000000000000000000000000000000000000000000", + "Input": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002", + "Expected": "000000000000000000000000000000000000000000000000000000000000000001", "Name": "geth-fail-length", "GasOld": 200, "GasNew": 1600 @@ -141,7 +141,7 @@ }, { "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000181000000000000000000000000000000000000000000000000000000000000000801ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2cffffffffffffffffffffffffffffffffffffffffffffffffffffffff3b10000000006c01ffffffffffffffffffffffffffffffffffffffffffffffdffffb97ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3bffffffffffffffffffffffffffffffffffffffffffffffffffffffffebafd93b37ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc5bb6affffffff3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2a", - "Expected": "0001000000000000", + "Expected": "0000000000000001", "Name": "guido-4-even", "GasOld": 1026, "GasNew": 94448 From 689b61a85c10e31f1b6b21412ac43ee1695a4249 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Tue, 12 Aug 2025 16:30:12 +0800 Subject: [PATCH 18/23] refactor: update comment --- .../eip7883_modexp_gas_increase/conftest.py | 22 +++++++++++++++---- .../osaka/eip7883_modexp_gas_increase/spec.py | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/conftest.py b/tests/osaka/eip7883_modexp_gas_increase/conftest.py index e171e3ee6d6..df395b90213 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/conftest.py +++ b/tests/osaka/eip7883_modexp_gas_increase/conftest.py @@ -26,7 +26,7 @@ def gas_new() -> int | None: @pytest.fixture def call_opcode() -> Op: - """Return call operationused to call the precompile.""" + """Return call operation used to call the precompile.""" return Op.CALL @@ -59,7 +59,16 @@ def gas_measure_contract( call_contract_post_storage: Storage, call_succeeds: bool, ) -> Address: - """Deploys a contract that measures ModExp gas consumption and execution result.""" + """ + Deploys a contract that measures ModExp gas consumption and execution result. + + Always stored: + storage[0]: precompile call success + storage[1]: return data length from precompile + Only if the precompile call succeeds: + storage[2]: gas consumed by precompile + storage[3]: hash of return data from precompile + """ assert call_opcode in [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL] value = [0] if call_opcode in [Op.CALL, Op.CALLCODE] else [] @@ -134,11 +143,16 @@ def precompile_gas( expected_gas = gas_old if fork < Osaka else gas_new assert calculated_gas == expected_gas, ( f"Calculated gas {calculated_gas} != Vector gas {expected_gas}\n" + f"Lengths: base: {hex(len(modexp_input.base))} ({len(modexp_input.base)}), " + f"exponent: {hex(len(modexp_input.exponent))} ({len(modexp_input.exponent)}), " + f"modulus: {hex(len(modexp_input.modulus))} ({len(modexp_input.modulus)})\n" + f"Exponent: {modexp_input.exponent} " + f"({int.from_bytes(modexp_input.exponent, byteorder='big')})" ) return calculated_gas except Exception as e: - print(f"Error calculating gas: {e}") - return 0 + print(f"Warning: Error calculating gas, using minimum: {e}") + return 500 if fork >= Osaka else 200 @pytest.fixture diff --git a/tests/osaka/eip7883_modexp_gas_increase/spec.py b/tests/osaka/eip7883_modexp_gas_increase/spec.py index b0556743c01..fd69cf1536e 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/spec.py +++ b/tests/osaka/eip7883_modexp_gas_increase/spec.py @@ -40,7 +40,7 @@ class Spec: EXPONENT_THRESHOLD = 32 GAS_DIVISOR = 3 - # Test Constants + # Arbitrary Test Constants modexp_input = ModExpInput( base="e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c0001020304050607", exponent="01ffffff", From fcadcb5f68fe6d42fdb7f822431d69fbff2a916e Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Thu, 21 Aug 2025 16:16:53 +0800 Subject: [PATCH 19/23] refactor: update naming and new cases --- .../test_modexp_thresholds.py | 166 +++++++++++------- 1 file changed, 107 insertions(+), 59 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index d1b88f5a81b..b4a9ceec8a9 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -26,7 +26,7 @@ vectors_from_file("vectors.json"), ids=lambda v: v.name, ) -def test_vectors_from_file( +def test_vectors_from_eip( state_test: StateTestFiller, pre: Alloc, tx: Transaction, @@ -99,47 +99,48 @@ def test_modexp_invalid_inputs( ) -def create_boundary_modexp_case( - base: str = "FF", exponent: str = "FF", modulus: str = "FF", case_id: str = "" -): - """ - Create a single boundary ModExp test case. - - Args: - base: Base data (hex string) - exponent: Exponent data (hex string) - modulus: Modulus data (hex string) - case_id: Test case identifier - - Returns: - pytest.param for the test case - - """ - modexp_input = ModExpInput( - base=base, - exponent=exponent, - modulus=modulus, - ) - return pytest.param(modexp_input, Spec.modexp_error, False, id=case_id) - - @pytest.mark.parametrize( "modexp_input,modexp_expected,call_succeeds", [ - create_boundary_modexp_case( - base="FF" * (Spec.MAX_LENGTH_BYTES + 1), case_id="base-too-long" + pytest.param( + ModExpInput( + base="FF" * (Spec.MAX_LENGTH_BYTES + 1), + exponent="FF", + modulus="FF", + ), + Spec.modexp_error, + False, + id="base-too-long", ), - create_boundary_modexp_case( - exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), case_id="exponent-too-long" + pytest.param( + ModExpInput( + base="FF", + exponent="FF" * (Spec.MAX_LENGTH_BYTES + 1), + modulus="FF", + ), + Spec.modexp_error, + False, + id="exponent-too-long", ), - create_boundary_modexp_case( - modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), case_id="modulus-too-long" + pytest.param( + ModExpInput( + base="FF", + exponent="FF", + modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), + ), + Spec.modexp_error, + False, + id="modulus-too-long", ), - create_boundary_modexp_case( - base="FF" * (Spec.MAX_LENGTH_BYTES + 1), - exponent="FF", - modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), - case_id="base-modulus-too-long", + pytest.param( + ModExpInput( + base="FF" * (Spec.MAX_LENGTH_BYTES + 1), + exponent="FF", + modulus="FF" * (Spec.MAX_LENGTH_BYTES + 1), + ), + Spec.modexp_error, + False, + id="base-modulus-too-long", ), ], ) @@ -219,13 +220,13 @@ def test_modexp_call_operations( @EIPChecklist.Precompile.Test.ValueTransfer.Fee.Over @EIPChecklist.Precompile.Test.ValueTransfer.Fee.Exact @EIPChecklist.Precompile.Test.ValueTransfer.Fee.Under -def test_modexp_gas_usage( +def test_modexp_gas_usage_contract_wrapper( state_test: StateTestFiller, pre: Alloc, tx: Transaction, post: Dict, ): - """Test ModExp gas cost with different precompile gas modifiers.""" + """Test ModExp gas cost with different gas modifiers using contract wrapper calls.""" state_test(pre=pre, tx=tx, post=post) @@ -255,14 +256,14 @@ def test_modexp_gas_usage( ), ], ) -def test_modexp_entry_points( +def test_modexp_used_in_transaction_entry_points( state_test: StateTestFiller, pre: Alloc, tx: Transaction, modexp_input: bytes, tx_gas_limit: int, ): - """Test ModExp entry points with different precompile gas modifiers.""" + """Test ModExp using in transaction entry points with different precompile gas modifiers.""" tx = Transaction( to=Spec.MODEXP_ADDRESS, sender=pre.fund_eoa(), @@ -296,6 +297,34 @@ def create_modexp_variable_gas_test_cases(): ("01" * 40, "00" * 39 + "01", "02" * 40, "01" * 40, "L3"), ("01" * 48, "01", "02" * 16, "01" * 16, "L4"), ("01" * 16, "00" * 40, "02" * 48, "00" * 47 + "01", "L5"), + # Critical 32-byte boundary cases + ("01" * 31, "01", "02" * 33, "00" * 2 + "01" * 31, "B1"), + ("01" * 33, "01", "02" * 31, "00" * 29 + "01" * 2, "B2"), + ("01" * 33, "01", "02" * 33, "01" * 33, "B4"), + # Zero value edge cases + ("00" * 32, "00" * 32, "01" * 32, "00" * 31 + "01", "Z1"), + ("01" * 32, "00" * 32, "00" * 32, "00" * 32, "Z2"), + ("00" * 32, "01" * 32, "02" * 32, "00" * 32, "Z3"), + # Maximum value stress tests + ("FF" * 64, "FF" * 64, "FF" * 64, "00" * 64, "M1"), + ("FF" * 32, "01", "FF" * 32, "00" * 32, "M2"), + ("01", "FF" * 64, "FF" * 64, "00" * 63 + "01", "M3"), + # Tiny maximum values + ("FF", "FE", "FD", "47", "T2"), + # Bit pattern cases + ("01" * 32, "80" * 32, "02" * 32, "01" * 32, "P2"), + ("01" * 33, "00" * 31 + "80" + "00", "02" * 33, "01" * 33, "P3"), + # Asymmetric length cases + ("01", "00" * 64, "02" * 64, "00" * 63 + "01", "A1"), + ("01" * 64, "01", "02", "01", "A2"), + ("01" * 64, "00" * 64, "02", "01", "A3"), + # Word boundary case + ("01" * 8, "01", "02" * 8, "0101010101010101", "W2"), + # Exponent edge cases + ("01" * 16, "00" * 32 + "01", "02" * 16, "01" * 16, "E1"), + ("01" * 16, "80" + "00" * 31, "02" * 16, "01" * 16, "E2"), + ("01" * 16, "00" * 31 + "80", "02" * 16, "01" * 16, "E3"), + ("01" * 16, "7F" + "FF" * 31, "02" * 16, "01" * 16, "E4"), ] # Gas calculation parameters: @@ -318,25 +347,44 @@ def create_modexp_variable_gas_test_cases(): # - Clamp: True if raw gas < 500 (clamped to 500), False if raw gas ≥ 500 (no clamping) # Test case coverage table: - # ┌─────┬──────┬─────┬──────┬───────┬─────────┬─────────────────────────────────────────────┐ - # │ ID │ Comp │ Rel │ Iter │ Clamp │ Gas │ Description │ - # ├─────┼──────┼─────┼──────┼───────┼─────────┼─────────────────────────────────────────────┤ - # │ Z0 │ - │ - │ - │ - │ 500 │ Zero case - empty inputs │ - # │ S0 │ S │ = │ A │ True │ 500 │ Small, equal, zero exponent, clamped │ - # │ S1 │ S │ = │ B │ True │ 500 │ Small, equal, small exp, clamped │ - # │ S2 │ S │ = │ B │ False │ 4080 │ Small, equal, large exp, unclamped │ - # │ S3 │ S │ = │ C │ False │ 2032 │ Small, equal, large exp+zero low256 │ - # │ S4 │ S │ = │ D │ False │ 2048 │ Small, equal, large exp+non-zero low256 │ - # │ S5 │ S │ > │ A │ True │ 500 │ Small, base>mod, zero exp, clamped │ - # │ S6 │ S │ < │ B │ True │ 500 │ Small, base │ B │ True │ 500 │ Large, base>mod, small exp, clamped │ - # │ L5 │ L │ < │ C │ False │ 9144 │ Large, base │ A │ True │ 500 │ Small, base > mod, zero exp, clamped │ + # │ S6 │ S │ < │ B │ True │ 500 │ Small, base < mod, small exp, clamped │ + # │ L0 │ L │ = │ A │ True │ 500 │ Large, equal, zero exp, clamped │ + # │ L1 │ L │ = │ B │ False │ 12750 │ Large, equal, large exp, unclamped │ + # │ L2 │ L │ = │ C │ False │ 6350 │ Large, equal, large exp + zero low256 │ + # │ L3 │ L │ = │ D │ False │ 6400 │ Large, equal, large exp + non-zero low256 │ + # │ L4 │ L │ > │ B │ True │ 500 │ Large, base > mod, small exp, clamped │ + # │ L5 │ L │ < │ C │ False │ 9144 │ Large, base < mod, large exp + zero low256 │ + # │ B1 │ L │ < │ B │ True │ 500 │ Cross 32-byte boundary (31/33) │ + # │ B2 │ L │ > │ B │ True │ 500 │ Cross 32-byte boundary (33/31) │ + # │ B4 │ L │ = │ B │ True │ 500 │ Just over 32-byte boundary │ + # │ Z1 │ S │ = │ A │ True │ 500 │ All zeros except modulus │ + # │ Z2 │ S │ = │ A │ True │ 500 │ Zero modulus special case │ + # │ Z3 │ S │ = │ B │ False │ 3968 │ Zero base, large exponent │ + # │ M1 │ L │ = │ D │ False │ 98176 │ Maximum values stress test │ + # │ M2 │ S │ = │ B │ True │ 500 │ Max base/mod, small exponent │ + # │ M3 │ L │ < │ D │ False │ 98176 │ Small base, max exponent/mod │ + # │ T2 │ S │ = │ B │ True │ 500 │ Tiny maximum values │ + # │ P2 │ S │ = │ B │ False │ 4080 │ High bit in exponent │ + # │ P3 │ L │ = │ D │ False │ 1550 │ Specific bit pattern in large exponent │ + # │ A1 │ L │ < │ C │ False │ 65408 │ Asymmetric: tiny base, large exp/mod │ + # │ A2 │ L │ > │ B │ True │ 500 │ Asymmetric: large base, tiny exp/mod │ + # │ A3 │ L │ > │ C │ False │ 65408 │ Asymmetric: large base/exp, tiny modulus │ + # │ W2 │ S │ = │ B │ True │ 500 │ Exactly 8-byte words │ + # │ E1 │ S │ = │ D │ True │ 500 │ Exponent exactly 33 bytes │ + # │ E2 │ S │ = │ B │ False │ 4080 │ High bit in exponent first byte │ + # │ E3 │ S │ = │ B │ True │ 500 │ High bit in exponent last byte │ + # │ E4 │ S │ = │ B │ False │ 4064 │ Maximum 32-byte exponent │ + # └─────┴──────┴─────┴──────┴───────┴─────────┴───────────────────────────────────────────────┘ for base, exponent, modulus, expected_result, test_id in test_cases: yield pytest.param( ModExpInput(base=base, exponent=exponent, modulus=modulus), From 8d0fc3e3149b13ddd582b8df2c19986dbdb889b6 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Thu, 21 Aug 2025 18:26:35 +0800 Subject: [PATCH 20/23] refactor: update valid fork configuration --- .../test_modexp_thresholds.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py index b4a9ceec8a9..3d7de96ada1 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py @@ -18,14 +18,13 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_7883.git_path REFERENCE_SPEC_VERSION = ref_spec_7883.version -pytestmark = pytest.mark.valid_from("Osaka") - @pytest.mark.parametrize( "modexp_input,modexp_expected,gas_old,gas_new", vectors_from_file("vectors.json"), ids=lambda v: v.name, ) +@pytest.mark.valid_from("Berlin") def test_vectors_from_eip( state_test: StateTestFiller, pre: Alloc, @@ -45,6 +44,7 @@ def test_vectors_from_eip( vectors_from_file("legacy.json"), ids=lambda v: v.name, ) +@pytest.mark.valid_from("Berlin") def test_vectors_from_legacy_tests( state_test: StateTestFiller, pre: Alloc, @@ -85,6 +85,7 @@ def test_vectors_from_legacy_tests( ], ) @EIPChecklist.Precompile.Test.Inputs.AllZeros +@pytest.mark.valid_from("Berlin") def test_modexp_invalid_inputs( state_test: StateTestFiller, pre: Alloc, @@ -144,6 +145,7 @@ def test_modexp_invalid_inputs( ), ], ) +@pytest.mark.valid_from("Osaka") def test_modexp_boundary_inputs( state_test: StateTestFiller, pre: Alloc, @@ -177,6 +179,7 @@ def test_modexp_boundary_inputs( @EIPChecklist.Precompile.Test.CallContexts.Delegate @EIPChecklist.Precompile.Test.CallContexts.Callcode @EIPChecklist.Precompile.Test.CallContexts.Normal +@pytest.mark.valid_from("Berlin") def test_modexp_call_operations( state_test: StateTestFiller, pre: Alloc, @@ -220,6 +223,7 @@ def test_modexp_call_operations( @EIPChecklist.Precompile.Test.ValueTransfer.Fee.Over @EIPChecklist.Precompile.Test.ValueTransfer.Fee.Exact @EIPChecklist.Precompile.Test.ValueTransfer.Fee.Under +@pytest.mark.valid_from("Berlin") def test_modexp_gas_usage_contract_wrapper( state_test: StateTestFiller, pre: Alloc, @@ -256,6 +260,7 @@ def test_modexp_gas_usage_contract_wrapper( ), ], ) +@pytest.mark.valid_from("Berlin") def test_modexp_used_in_transaction_entry_points( state_test: StateTestFiller, pre: Alloc, @@ -397,6 +402,7 @@ def create_modexp_variable_gas_test_cases(): "modexp_input,modexp_expected", create_modexp_variable_gas_test_cases(), ) +@pytest.mark.valid_from("Berlin") def test_modexp_variable_gas_cost( state_test: StateTestFiller, pre: Alloc, From 60cf012fd425bdec7ca4e69eea6bf3bcb178183d Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Fri, 22 Aug 2025 13:16:52 +0000 Subject: [PATCH 21/23] chore(ci): try diff approach. --- .github/scripts/fill_prepatched_tests.sh | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/.github/scripts/fill_prepatched_tests.sh b/.github/scripts/fill_prepatched_tests.sh index fc572b15d90..f86a1ba68e2 100755 --- a/.github/scripts/fill_prepatched_tests.sh +++ b/.github/scripts/fill_prepatched_tests.sh @@ -25,23 +25,15 @@ echo "Select files that were changed and exist on the main branch:" echo "$MODIFIED_DELETED_FILES" rm -rf fixtures -rm -f filloutput.log -uv run fill $MODIFIED_DELETED_FILES --clean --until=$FILL_UNTIL --evm-bin evmone-t8n --block-gas-limit $BLOCK_GAS_LIMIT -m "state_test or blockchain_test" --output $BASE_TEST_PATH > >(tee -a filloutput.log) 2> >(tee -a filloutput.log >&2) - -if grep -q "no tests ran" filloutput.log; then +uv run fill $MODIFIED_DELETED_FILES --clean --until=$FILL_UNTIL --evm-bin evmone-t8n --block-gas-limit $BLOCK_GAS_LIMIT -m "state_test or blockchain_test" --output $BASE_TEST_PATH +FILL_RETURN_CODE=$? +if [ $FILL_RETURN_CODE -eq 5 ]; then echo "any_modified_fixtures=false" >> "$GITHUB_OUTPUT" exit 0 -fi - -if grep -q "FAILURES" filloutput.log; then - echo "Error: failed to generate .py tests from before the PR." - exit 1 -fi - -if grep -q "ERROR collecting test session" filloutput.log; then +elif [ $FILL_RETURN_CODE -ne 0 ]; then echo "Error: failed to generate .py tests from before the PR." - exit 1 + exit $FILL_RETURN_CODE fi git checkout $PATCH_COMMIT From 32bc96771812e6131789fa8e7e85cafc849e14c4 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Fri, 22 Aug 2025 13:49:26 +0000 Subject: [PATCH 22/23] chore --- .github/scripts/fill_prepatched_tests.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/scripts/fill_prepatched_tests.sh b/.github/scripts/fill_prepatched_tests.sh index f86a1ba68e2..97bd5824d87 100755 --- a/.github/scripts/fill_prepatched_tests.sh +++ b/.github/scripts/fill_prepatched_tests.sh @@ -28,6 +28,7 @@ rm -rf fixtures uv run fill $MODIFIED_DELETED_FILES --clean --until=$FILL_UNTIL --evm-bin evmone-t8n --block-gas-limit $BLOCK_GAS_LIMIT -m "state_test or blockchain_test" --output $BASE_TEST_PATH FILL_RETURN_CODE=$? +echo $FILL_RETURN_CODE if [ $FILL_RETURN_CODE -eq 5 ]; then echo "any_modified_fixtures=false" >> "$GITHUB_OUTPUT" exit 0 From 7d5769f95370ffd8cdbf2b87f19d8c72a09c610f Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Fri, 22 Aug 2025 14:06:02 +0000 Subject: [PATCH 23/23] chore: set -e/+e. --- .github/scripts/fill_prepatched_tests.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/fill_prepatched_tests.sh b/.github/scripts/fill_prepatched_tests.sh index 97bd5824d87..bec78e0c7c8 100755 --- a/.github/scripts/fill_prepatched_tests.sh +++ b/.github/scripts/fill_prepatched_tests.sh @@ -26,9 +26,10 @@ echo "$MODIFIED_DELETED_FILES" rm -rf fixtures +set +e uv run fill $MODIFIED_DELETED_FILES --clean --until=$FILL_UNTIL --evm-bin evmone-t8n --block-gas-limit $BLOCK_GAS_LIMIT -m "state_test or blockchain_test" --output $BASE_TEST_PATH FILL_RETURN_CODE=$? -echo $FILL_RETURN_CODE +set -e if [ $FILL_RETURN_CODE -eq 5 ]; then echo "any_modified_fixtures=false" >> "$GITHUB_OUTPUT" exit 0