From a2538fa1af12d224d48393ddd7f9e6a20e28abe2 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 15 Sep 2025 15:44:43 +0800 Subject: [PATCH 1/4] feat(tests): enhance eip7823 coverage --- .../eip7823_modexp_upper_bounds/conftest.py | 187 +++++++++++++ .../osaka/eip7823_modexp_upper_bounds/spec.py | 41 +++ .../test_modexp_upper_bounds.py | 245 +++++++++++------- 3 files changed, 384 insertions(+), 89 deletions(-) create mode 100644 tests/osaka/eip7823_modexp_upper_bounds/conftest.py create mode 100644 tests/osaka/eip7823_modexp_upper_bounds/spec.py diff --git a/tests/osaka/eip7823_modexp_upper_bounds/conftest.py b/tests/osaka/eip7823_modexp_upper_bounds/conftest.py new file mode 100644 index 00000000000..bcd318fd99a --- /dev/null +++ b/tests/osaka/eip7823_modexp_upper_bounds/conftest.py @@ -0,0 +1,187 @@ +"""Conftest for EIP-7823 tests.""" + +from typing import Dict + +import pytest + +from ethereum_test_forks import Fork, Osaka +from ethereum_test_tools import Account, Address, Alloc, Bytes, Storage, Transaction, keccak256 +from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_types import Environment + +from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput +from ..eip7883_modexp_gas_increase.spec import Spec as Spec7883 +from .spec import Spec + + +@pytest.fixture +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 call_succeeds( + total_gas_used: int, fork: Fork, env: Environment, modexp_input: ModExpInput +) -> bool: + """ + By default, depending on the expected output, we can deduce if the call is expected to succeed + or fail. + """ + # Transaction gas limit exceeded + tx_gas_limit_cap = fork.transaction_gas_limit_cap() or env.gas_limit + if total_gas_used > tx_gas_limit_cap: + return False + + # Input length exceeded + base_length, exp_length, mod_length = modexp_input.get_declared_lengths() + if ( + base_length > Spec.MAX_LENGTH_BYTES + or exp_length > Spec.MAX_LENGTH_BYTES + or mod_length > Spec.MAX_LENGTH_BYTES + ) and fork >= Osaka: + return False + + return True + + +@pytest.fixture +def gas_measure_contract( + pre: Alloc, + fork: Fork, + modexp_expected: bytes, + precompile_gas: int, + call_contract_post_storage: Storage, + call_succeeds: bool, +) -> Address: + """ + 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 + """ + call_code = Op.CALL( + precompile_gas, + Spec.MODEXP_ADDRESS, + 0, + 0, + Op.CALLDATASIZE(), + 0, + 0, + ) + + 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 # CALLDATASIZE + + gas_costs.G_BASE # GAS + ) + + # 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.SSTORE(call_contract_post_storage.store_next(call_succeeds), call_result_measurement) + + Op.SSTORE( + call_contract_post_storage.store_next(len(modexp_expected) if call_succeeds else 0), + Op.RETURNDATASIZE(), + ) + ) + + if call_succeeds: + code += Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation) + 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) + + +@pytest.fixture +def precompile_gas(fork: Fork, modexp_input: ModExpInput) -> int: + """Calculate gas cost for the ModExp precompile and verify it matches expected gas.""" + spec = Spec if fork < Osaka else Spec7883 + try: + calculated_gas = spec.calculate_gas_cost(modexp_input) + return calculated_gas + except Exception: + # Used for `test_modexp_invalid_inputs` we expect the call to not succeed. + # Return is for completeness. + return 500 if fork >= Osaka else 200 + + +@pytest.fixture +def tx( + pre: Alloc, + gas_measure_contract: Address, + modexp_input: ModExpInput, + tx_gas_limit: int, +) -> Transaction: + """Transaction to measure gas consumption of the ModExp precompile.""" + return Transaction( + sender=pre.fund_eoa(), + to=gas_measure_contract, + data=bytes(modexp_input), + gas_limit=tx_gas_limit, + ) + + +@pytest.fixture +def total_gas_used( + 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() + extra_gas = 500_000 + + total_gas = ( + extra_gas + + intrinsic_gas_cost_calculator(calldata=bytes(modexp_input)) + + memory_expansion_gas_calculator(new_bytes=len(bytes(modexp_input))) + + precompile_gas + ) + + return total_gas + + +@pytest.fixture +def tx_gas_limit(total_gas_used: int, fork: Fork, env: Environment) -> int: + """Transaction gas limit used for the test (Can be overridden in the test).""" + tx_gas_limit_cap = fork.transaction_gas_limit_cap() or env.gas_limit + return min(tx_gas_limit_cap, total_gas_used) + + +@pytest.fixture +def post( + gas_measure_contract: Address, + call_contract_post_storage: Storage, +) -> Dict[Address, Account]: + """Return expected post state with gas consumption check.""" + return { + gas_measure_contract: Account(storage=call_contract_post_storage), + } diff --git a/tests/osaka/eip7823_modexp_upper_bounds/spec.py b/tests/osaka/eip7823_modexp_upper_bounds/spec.py new file mode 100644 index 00000000000..d99e7fed20a --- /dev/null +++ b/tests/osaka/eip7823_modexp_upper_bounds/spec.py @@ -0,0 +1,41 @@ +"""Defines EIP-7823 specification constants and functions.""" + +from dataclasses import dataclass + +from ..eip7883_modexp_gas_increase.spec import Spec, ceiling_division + + +@dataclass(frozen=True) +class ReferenceSpec: + """Defines the reference spec version and git path.""" + + git_path: str + version: str + + +ref_spec_7823 = ReferenceSpec("EIPS/eip-7823.md", "c8321494fdfbfda52ad46c3515a7ca5dc86b857c") + + +@dataclass(frozen=True) +class Spec7823(Spec): + """ + Constants and helpers for the ModExp gas cost increase EIP. + These override the original Spec class variables for EIP-7823. + """ + + MODEXP_ADDRESS = 0x05 + MIN_GAS = 500 + + LARGE_BASE_MODULUS_MULTIPLIER = 2 + EXPONENT_BYTE_MULTIPLIER = 16 + GAS_DIVISOR = 1 + + @classmethod + def calculate_multiplication_complexity(cls, base_length: int, modulus_length: int) -> int: + """Calculate the multiplication complexity of the ModExp precompile for EIP-7883.""" + max_length = max(base_length, modulus_length) + words = ceiling_division(max_length, cls.WORD_SIZE) + complexity = 16 + if max_length > cls.MAX_LENGTH_THRESHOLD: + complexity = cls.LARGE_BASE_MODULUS_MULTIPLIER * words**2 + return complexity diff --git a/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py b/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py index 6ac1a742701..38cb1717966 100644 --- a/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py +++ b/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py @@ -3,121 +3,143 @@ Tests upper bounds of the MODEXP precompile. """ +from typing import Dict + import pytest -from ethereum_test_forks import Fork, Osaka -from ethereum_test_tools import Account, Alloc, Bytes, Environment, StateTestFiller, Transaction +from ethereum_test_forks import Fork +from ethereum_test_tools import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + Bytes, + StateTestFiller, + Transaction, + keccak256, +) from ethereum_test_tools.vm.opcode import Opcodes as Op from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput, ModExpOutput -from ..eip7883_modexp_gas_increase.spec import Spec, Spec7883 - -REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7823.md" -REFERENCE_SPEC_VERSION = "c8321494fdfbfda52ad46c3515a7ca5dc86b857c" - -MAX_LENGTH_BYTES = 1024 -TX_GAS_LIMIT = 2**24 +from .spec import Spec, ref_spec_7823 +REFERENCE_SPEC_GIT_PATH = ref_spec_7823.git_path +REFERENCE_SPEC_VERSION = ref_spec_7823.version -@pytest.fixture -def precompile_gas(fork: Fork, mod_exp_input: ModExpInput) -> int: - """Calculate gas cost for the ModExp precompile and verify it matches expected gas.""" - spec = Spec if fork < Osaka else Spec7883 - calculated_gas = spec.calculate_gas_cost(mod_exp_input) - return calculated_gas - -@pytest.mark.valid_from("Prague") +@pytest.mark.valid_from("Osaka") @pytest.mark.parametrize( - "mod_exp_input", + "modexp_input,modexp_expected,call_succeeds", [ pytest.param( ModExpInput( - base=b"\0" * (MAX_LENGTH_BYTES + 1), + base=b"\0" * (Spec.MAX_LENGTH_BYTES + 1), exponent=b"\0", modulus=b"\2", ), + Spec.modexp_error, + False, id="excess_length_base", ), pytest.param( ModExpInput( base=b"\0", - exponent=b"\0" * (MAX_LENGTH_BYTES + 1), + exponent=b"\0" * (Spec.MAX_LENGTH_BYTES + 1), modulus=b"\2", ), + Spec.modexp_error, + False, id="excess_length_exponent", ), pytest.param( ModExpInput( base=b"\0", exponent=b"\0", - modulus=b"\0" * (MAX_LENGTH_BYTES) + b"\2", + modulus=b"\0" * (Spec.MAX_LENGTH_BYTES) + b"\2", ), + Spec.modexp_error, + False, id="excess_length_modulus", ), pytest.param( ModExpInput( base=b"", - exponent=b"\0" * (MAX_LENGTH_BYTES + 1), + exponent=b"\0" * (Spec.MAX_LENGTH_BYTES + 1), modulus=b"", ), + Spec.modexp_error, + False, id="exp_1025_base_0_mod_0", ), pytest.param( ModExpInput( base=b"", # Non-zero exponent is cancelled with zero multiplication complexity pre EIP-7823. - exponent=b"\xff" * (MAX_LENGTH_BYTES + 1), + exponent=b"\xff" * (Spec.MAX_LENGTH_BYTES + 1), modulus=b"", ), + Spec.modexp_error, + False, id="expFF_1025_base_0_mod_0", ), pytest.param( ModExpInput( - base=b"\0" * MAX_LENGTH_BYTES, - exponent=b"\xff" * (MAX_LENGTH_BYTES + 1), + base=b"\0" * Spec.MAX_LENGTH_BYTES, + exponent=b"\xff" * (Spec.MAX_LENGTH_BYTES + 1), modulus=b"", ), + Spec.modexp_error, + False, id="expFF_1025_base_1024_mod_0", ), pytest.param( ModExpInput( - base=b"\0" * (MAX_LENGTH_BYTES + 1), - exponent=b"\xff" * (MAX_LENGTH_BYTES + 1), + base=b"\0" * (Spec.MAX_LENGTH_BYTES + 1), + exponent=b"\xff" * (Spec.MAX_LENGTH_BYTES + 1), modulus=b"", ), + Spec.modexp_error, + False, id="expFF_1025_base_1025_mod_0", ), pytest.param( ModExpInput( - base=b"\0" * (MAX_LENGTH_BYTES + 1), + base=b"\0" * (Spec.MAX_LENGTH_BYTES + 1), exponent=b"", modulus=b"", ), + Spec.modexp_error, + False, id="exp_0_base_1025_mod_0", ), pytest.param( ModExpInput( - base=b"\0" * (MAX_LENGTH_BYTES + 1), + base=b"\0" * (Spec.MAX_LENGTH_BYTES + 1), exponent=b"", modulus=b"\2", ), + Spec.modexp_error, + False, id="exp_0_base_1025_mod_1", ), pytest.param( ModExpInput( base=b"", exponent=b"", - modulus=b"\0" * (MAX_LENGTH_BYTES + 1), + modulus=b"\0" * (Spec.MAX_LENGTH_BYTES + 1), ), + Spec.modexp_error, + False, id="exp_0_base_0_mod_1025", ), pytest.param( ModExpInput( base=b"\1", exponent=b"", - modulus=b"\0" * (MAX_LENGTH_BYTES + 1), + modulus=b"\0" * (Spec.MAX_LENGTH_BYTES + 1), ), + Spec.modexp_error, + False, id="exp_0_base_1_mod_1025", ), pytest.param( @@ -127,32 +149,40 @@ def precompile_gas(fork: Fork, mod_exp_input: ModExpInput) -> int: modulus=b"", declared_exponent_length=2**64, ), + Spec.modexp_error, + False, id="exp_2_pow_64_base_0_mod_0", ), # Implementation coverage tests pytest.param( ModExpInput( - base=b"\xff" * (MAX_LENGTH_BYTES + 1), - exponent=b"\xff" * (MAX_LENGTH_BYTES + 1), - modulus=b"\xff" * (MAX_LENGTH_BYTES + 1), + base=b"\xff" * (Spec.MAX_LENGTH_BYTES + 1), + exponent=b"\xff" * (Spec.MAX_LENGTH_BYTES + 1), + modulus=b"\xff" * (Spec.MAX_LENGTH_BYTES + 1), ), + Spec.modexp_error, + False, id="all_exceed_check_ordering", ), pytest.param( ModExpInput( - base=b"\x00" * MAX_LENGTH_BYTES, - exponent=b"\xff" * (MAX_LENGTH_BYTES + 1), - modulus=b"\xff" * (MAX_LENGTH_BYTES + 1), + base=b"\x00" * Spec.MAX_LENGTH_BYTES, + exponent=b"\xff" * (Spec.MAX_LENGTH_BYTES + 1), + modulus=b"\xff" * (Spec.MAX_LENGTH_BYTES + 1), ), + Spec.modexp_error, + False, id="exp_mod_exceed_base_ok", ), pytest.param( ModExpInput( # Bitwise pattern for Nethermind optimization - base=b"\xaa" * (MAX_LENGTH_BYTES + 1), - exponent=b"\x55" * MAX_LENGTH_BYTES, - modulus=b"\xff" * MAX_LENGTH_BYTES, + base=b"\xaa" * (Spec.MAX_LENGTH_BYTES + 1), + exponent=b"\x55" * Spec.MAX_LENGTH_BYTES, + modulus=b"\xff" * Spec.MAX_LENGTH_BYTES, ), + Spec.modexp_error, + False, id="bitwise_pattern_base_exceed", ), pytest.param( @@ -165,76 +195,113 @@ def precompile_gas(fork: Fork, mod_exp_input: ModExpInput) -> int: declared_exponent_length=1, declared_modulus_length=1, ), + Spec.modexp_error, + False, id="near_uint64_max_base", ), pytest.param( ModExpInput( - base=b"\x01" * MAX_LENGTH_BYTES, + base=b"\x01" * Spec.MAX_LENGTH_BYTES, exponent=b"", - modulus=b"\x02" * (MAX_LENGTH_BYTES + 1), + modulus=b"\x02" * (Spec.MAX_LENGTH_BYTES + 1), declared_exponent_length=0, ), + Spec.modexp_error, + False, id="zero_exp_mod_exceed", ), ], ) -def test_modexp_upper_bounds( +def test_modexp_input_bounds( state_test: StateTestFiller, - mod_exp_input: ModExpInput, + modexp_input: ModExpInput, + modexp_expected: ModExpOutput, precompile_gas: int, fork: Fork, + tx: Transaction, + post: Dict, pre: Alloc, ): - """Test the MODEXP precompile.""" - sender = pre.fund_eoa() + """Test the MODEXP precompile input bounds.""" + state_test(pre=pre, tx=tx, post=post) - account = pre.deploy_contract( - # Store all CALLDATA into memory (offset 0) - Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE()) - # Store the returned CALL status (success = 1, fail = 0) into slot 0: - + Op.SSTORE( - 0, - # Setup stack to CALL into ModExp with the CALLDATA and CALL into it (+ pop value) - Op.CALL( - gas=precompile_gas, - address=0x05, - value=0, - args_offset=0, - args_size=Op.CALLDATASIZE(), + +@pytest.mark.parametrize( + "modexp_input,modexp_expected", + [ + pytest.param( + ModExpInput( + base=b"\1" * (Spec.MAX_LENGTH_BYTES + 1), + exponent=b"\0", + modulus=b"\2", ), - ) - # STOP (handy for tracing) - + Op.STOP(), + b"\1", + id="base_1_exp_0_mod_2", + ), + ], +) +@pytest.mark.valid_at_transition_to("Osaka", subsequent_forks=True) +def test_modexp_upper_bounds_fork_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + precompile_gas: int, + modexp_input: ModExpInput, + modexp_expected: bytes, +): + """Test MODEXP upper bounds enforcement transition from before to after Osaka hard fork.""" + call_code = Op.CALL( + address=Spec.MODEXP_ADDRESS, + args_size=Op.CALLDATASIZE, ) - intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas_cost = intrinsic_gas_cost_calc(calldata=mod_exp_input) - memory_expansion_gas_calc = fork.memory_expansion_gas_calculator() - memory_expansion_gas = memory_expansion_gas_calc(new_bytes=len(bytes(mod_exp_input))) + code = ( + Op.CALLDATACOPY(size=Op.CALLDATASIZE) + + Op.SSTORE( + Op.TIMESTAMP, + call_code, + ) + + Op.RETURNDATACOPY(size=Op.RETURNDATASIZE()) + + Op.SSTORE( + Op.AND(Op.TIMESTAMP, 0xFF), + Op.SHA3(0, Op.RETURNDATASIZE()), + ) + ) - gas_limit = intrinsic_gas_cost + (precompile_gas * 64 // 63) + memory_expansion_gas + 100_000 - expensive = gas_limit > TX_GAS_LIMIT - gas_limit = TX_GAS_LIMIT if expensive else gas_limit - env = Environment(gas_limit=gas_limit) + senders = [pre.fund_eoa() for _ in range(3)] + contracts = [pre.deploy_contract(code) for _ in range(3)] + timestamps = [14_999, 15_000, 15_001] # Before, at, and after transition + expected_results = [True, False, False] - tx = Transaction( - ty=0x0, - to=account, - data=mod_exp_input, - gas_limit=gas_limit, - protected=True, - sender=sender, - ) - base_length, exp_length, mod_length = mod_exp_input.get_declared_lengths() - if ( - base_length <= MAX_LENGTH_BYTES - and exp_length <= MAX_LENGTH_BYTES - and mod_length <= MAX_LENGTH_BYTES - ) or (fork < Osaka and not expensive): - output = ModExpOutput(call_success=True, returned_data="0x01") - else: - output = ModExpOutput(call_success=False, returned_data="0x") + blocks = [ + Block( + timestamp=ts, + txs=[ + Transaction( + to=contract, + data=bytes(modexp_input), + sender=sender, + gas_limit=6_000_000, + ) + ], + ) + for ts, contract, sender in zip(timestamps, contracts, senders, strict=False) + ] - post = {account: Account(storage={0: output.call_success})} + post = { + contract: Account( + storage={ + ts: expected, + ts & 0xFF: keccak256(modexp_expected) + if expected + else keccak256(Spec.modexp_error), + } + ) + for contract, ts, expected in zip(contracts, timestamps, expected_results, strict=False) + } - state_test(env=env, pre=pre, post=post, tx=tx) + blockchain_test( + pre=pre, + blocks=blocks, + post=post, + ) From 5764b7df79734e079aa56b76b1058e66776c770e Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 15 Sep 2025 15:45:39 +0800 Subject: [PATCH 2/4] feat(tests): add eip7823 checklist items for coverage --- .../eip_checklist_external_coverage.txt | 1 + .../eip_checklist_not_applicable.txt | 25 +++++++++++++++++++ .../eip7702_set_code_tx/test_set_code_txs.py | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_external_coverage.txt create mode 100644 tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_not_applicable.txt diff --git a/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_external_coverage.txt b/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_external_coverage.txt new file mode 100644 index 00000000000..5a977d92882 --- /dev/null +++ b/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_external_coverage.txt @@ -0,0 +1 @@ +precompile/test/call_contexts/set_code = Covered in EIP-7702 cases \ No newline at end of file diff --git a/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_not_applicable.txt b/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_not_applicable.txt new file mode 100644 index 00000000000..cc639b09052 --- /dev/null +++ b/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_not_applicable.txt @@ -0,0 +1,25 @@ +precompile/test/inputs/valid/crypto = No specific crypto property for Modexp +precompile/test/value_transfer/fee/under = No value is required +precompile/test/value_transfer/fee/exact = No value is required +precompile/test/value_transfer/fee/over = No value is required +precompile/test/input_lengths/static/correct = The Modexp input length is not static +precompile/test/input_lengths/static/too_short = The Modexp input length is not static +precompile/test/input_lengths/static/too_long = The Modexp input length is not static +precompile/test/input_lengths/dynamic/too_short = there would be no padding for precompile +precompile/test/gas_usage/constant/oog = The Modexp gas cost is dynamic +precompile/test/gas_usage/constant/exact = The Modexp gas cost is dynamic +precompile/test/fork_transition/before/invalid_input = Modexp is not new precompile, it is still valid befork fork activation +precompile/test/fork_transition/before/zero_gas = Modexp is not new precompile, it is still valid befork fork activation +precompile/test/fork_transition/before/cold = Modexp is not new precompile, it is still valid befork fork activation +gas_cost_changes/test/out_of_gas = No Out-of-gas scenario in Modexp +system_contract = EIP does not include a new system contract +opcode = EIP does not introduce a new opcode +removed_precompile = EIP does not remove a precompile +transaction_type = EIP does not introduce a new transaction type +block_header_field = EIP does not add any new block header fields +gas_refunds_changes = EIP does not introduce any gas refund changes +blob_count_changes = EIP does not introduce any blob count changes +execution_layer_request = EIP does not introduce an execution layer request +new_transaction_validity_constraint = EIP does not introduce a new transaction validity constraint +modified_transaction_validity_constraint = EIP does not introduce a modified transaction validity constraint +block_body_field = EIP does not add any new block body fields 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 c8d382cb9a7..cc25e6e57cc 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 @@ -2589,7 +2589,7 @@ def test_set_code_to_log( @pytest.mark.with_all_call_opcodes @pytest.mark.with_all_precompiles -@EIPChecklist.Precompile.Test.CallContexts.SetCode(eip=[7951, 7883]) +@EIPChecklist.Precompile.Test.CallContexts.SetCode(eip=[7951, 7883, 7823]) def test_set_code_to_precompile( state_test: StateTestFiller, pre: Alloc, From 016c31bfd739b124d01583dc309ea8b20481b3a1 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 15 Sep 2025 17:19:56 +0800 Subject: [PATCH 3/4] refactor(tests): update modexp upper bounds framework --- .../eip7823_modexp_upper_bounds/conftest.py | 9 +++---- .../osaka/eip7823_modexp_upper_bounds/spec.py | 27 ------------------- .../test_modexp_upper_bounds.py | 7 ++--- 3 files changed, 8 insertions(+), 35 deletions(-) diff --git a/tests/osaka/eip7823_modexp_upper_bounds/conftest.py b/tests/osaka/eip7823_modexp_upper_bounds/conftest.py index bcd318fd99a..56bfb1c9371 100644 --- a/tests/osaka/eip7823_modexp_upper_bounds/conftest.py +++ b/tests/osaka/eip7823_modexp_upper_bounds/conftest.py @@ -5,13 +5,12 @@ import pytest from ethereum_test_forks import Fork, Osaka -from ethereum_test_tools import Account, Address, Alloc, Bytes, Storage, Transaction, keccak256 +from ethereum_test_tools import Account, Address, Alloc, Storage, Transaction, keccak256 from ethereum_test_tools.vm.opcode import Opcodes as Op from ethereum_test_types import Environment from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput -from ..eip7883_modexp_gas_increase.spec import Spec as Spec7883 -from .spec import Spec +from ..eip7883_modexp_gas_increase.spec import Spec, Spec7883 @pytest.fixture @@ -80,7 +79,7 @@ def gas_measure_contract( 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_VERY_LOW * (len(Op.CALL.kwargs) - 1)) # type: ignore + gas_costs.G_BASE # CALLDATASIZE + gas_costs.G_BASE # GAS ) @@ -115,7 +114,7 @@ def gas_measure_contract( code += Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation) code += Op.RETURNDATACOPY(dest_offset=0, offset=0, size=Op.RETURNDATASIZE()) code += Op.SSTORE( - call_contract_post_storage.store_next(keccak256(Bytes(modexp_expected))), + 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/eip7823_modexp_upper_bounds/spec.py b/tests/osaka/eip7823_modexp_upper_bounds/spec.py index d99e7fed20a..88fddca02a7 100644 --- a/tests/osaka/eip7823_modexp_upper_bounds/spec.py +++ b/tests/osaka/eip7823_modexp_upper_bounds/spec.py @@ -2,8 +2,6 @@ from dataclasses import dataclass -from ..eip7883_modexp_gas_increase.spec import Spec, ceiling_division - @dataclass(frozen=True) class ReferenceSpec: @@ -14,28 +12,3 @@ class ReferenceSpec: ref_spec_7823 = ReferenceSpec("EIPS/eip-7823.md", "c8321494fdfbfda52ad46c3515a7ca5dc86b857c") - - -@dataclass(frozen=True) -class Spec7823(Spec): - """ - Constants and helpers for the ModExp gas cost increase EIP. - These override the original Spec class variables for EIP-7823. - """ - - MODEXP_ADDRESS = 0x05 - MIN_GAS = 500 - - LARGE_BASE_MODULUS_MULTIPLIER = 2 - EXPONENT_BYTE_MULTIPLIER = 16 - GAS_DIVISOR = 1 - - @classmethod - def calculate_multiplication_complexity(cls, base_length: int, modulus_length: int) -> int: - """Calculate the multiplication complexity of the ModExp precompile for EIP-7883.""" - max_length = max(base_length, modulus_length) - words = ceiling_division(max_length, cls.WORD_SIZE) - complexity = 16 - if max_length > cls.MAX_LENGTH_THRESHOLD: - complexity = cls.LARGE_BASE_MODULUS_MULTIPLIER * words**2 - return complexity diff --git a/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py b/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py index 38cb1717966..ba1b53a3611 100644 --- a/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py +++ b/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py @@ -20,8 +20,9 @@ ) from ethereum_test_tools.vm.opcode import Opcodes as Op -from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput, ModExpOutput -from .spec import Spec, ref_spec_7823 +from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput +from ..eip7883_modexp_gas_increase.spec import Spec +from .spec import ref_spec_7823 REFERENCE_SPEC_GIT_PATH = ref_spec_7823.git_path REFERENCE_SPEC_VERSION = ref_spec_7823.version @@ -215,7 +216,7 @@ def test_modexp_input_bounds( state_test: StateTestFiller, modexp_input: ModExpInput, - modexp_expected: ModExpOutput, + modexp_expected: bytes, precompile_gas: int, fork: Fork, tx: Transaction, From 2eb62163043da0b1e63aff53375c05d26b17b06b Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 15 Sep 2025 17:20:20 +0800 Subject: [PATCH 4/4] feat(tests): expand checklist item and enhance test coverage --- .../eip_checklist_external_coverage.txt | 28 +++++++++++- .../eip_checklist_not_applicable.txt | 3 +- .../test_modexp_upper_bounds.py | 45 ++++++++++++++++++- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_external_coverage.txt b/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_external_coverage.txt index 5a977d92882..213471321d1 100644 --- a/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_external_coverage.txt +++ b/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_external_coverage.txt @@ -1 +1,27 @@ -precompile/test/call_contexts/set_code = Covered in EIP-7702 cases \ No newline at end of file +precompile/test/call_contexts/set_code = Covered in EIP-7702 cases +precompile/test/call_contexts/normal = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/call_contexts/delegate = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/call_contexts/static = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/call_contexts/callcode = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/call_contexts/tx_entry = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/call_contexts/initcode/CREATE = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/call_contexts/initcode/tx = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/call_contexts/set_code = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/inputs/valid = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/inputs/valid/boundary = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/inputs/all_zeros = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/inputs/invalid = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/inputs/invalid/crypto = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/inputs/invalid/corrupted = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/value_transfer/no_fee = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/out_of_bounds/max_plus_one = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/input_lengths/zero = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/input_lengths/dynamic/valid = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/input_lengths/dynamic/too_long = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/gas_usage/dynamic/exact = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/gas_usage/dynamic/oog = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/excessive_gas_usage = Covered in osaka/eip7883_modexp_gas_increase +precompile/test/fork_transition/after/warm = Covered in osaka/eip7883_modexp_gas_increase +gas_cost_changes/test/gas_updates_measurement = Covered in osaka/eip7883_modexp_gas_increase +gas_cost_changes/test/fork_transition/before = Covered in osaka/eip7883_modexp_gas_increase +gas_cost_changes/test/fork_transition/after = Covered in osaka/eip7883_modexp_gas_increase \ No newline at end of file diff --git a/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_not_applicable.txt b/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_not_applicable.txt index cc639b09052..2fe9ebdfdc6 100644 --- a/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_not_applicable.txt +++ b/tests/osaka/eip7823_modexp_upper_bounds/eip_checklist_not_applicable.txt @@ -1,4 +1,3 @@ -precompile/test/inputs/valid/crypto = No specific crypto property for Modexp precompile/test/value_transfer/fee/under = No value is required precompile/test/value_transfer/fee/exact = No value is required precompile/test/value_transfer/fee/over = No value is required @@ -22,4 +21,4 @@ blob_count_changes = EIP does not introduce any blob count changes execution_layer_request = EIP does not introduce an execution layer request new_transaction_validity_constraint = EIP does not introduce a new transaction validity constraint modified_transaction_validity_constraint = EIP does not introduce a modified transaction validity constraint -block_body_field = EIP does not add any new block body fields +block_body_field = EIP does not add any new block body fields \ No newline at end of file diff --git a/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py b/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py index ba1b53a3611..2f32f1b94d7 100644 --- a/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py +++ b/tests/osaka/eip7823_modexp_upper_bounds/test_modexp_upper_bounds.py @@ -7,6 +7,7 @@ import pytest +from ethereum_test_checklists import EIPChecklist from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, @@ -211,9 +212,51 @@ False, id="zero_exp_mod_exceed", ), + pytest.param( + ModExpInput( + base=b"\x01" * Spec.MAX_LENGTH_BYTES, + exponent=b"\x00", + modulus=b"\x02", + ), + b"\x01", + True, + id="base_boundary", + ), + pytest.param( + ModExpInput( + base=b"\x01", + exponent=b"\x00" * Spec.MAX_LENGTH_BYTES, + modulus=b"\x02", + ), + b"\x01", + True, + id="exp_boundary", + ), + pytest.param( + ModExpInput( + base=b"\x01", + exponent=b"\x00", + modulus=b"\x02" * Spec.MAX_LENGTH_BYTES, + ), + b"\x01".rjust(Spec.MAX_LENGTH_BYTES, b"\x00"), + True, + id="mod_boundary", + ), + pytest.param( + ModExpInput( + base=b"\x01" * Spec.MAX_LENGTH_BYTES, + exponent=b"\x00", + modulus=b"\x02" * Spec.MAX_LENGTH_BYTES, + ), + b"\x01".rjust(Spec.MAX_LENGTH_BYTES, b"\x00"), + True, + id="base_mod_boundary", + ), ], ) -def test_modexp_input_bounds( +@EIPChecklist.Precompile.Test.Inputs.MaxValues +@EIPChecklist.Precompile.Test.OutOfBounds.Max +def test_modexp_upper_bounds( state_test: StateTestFiller, modexp_input: ModExpInput, modexp_expected: bytes,