From 7084c84cd107134d857ec3ee4f4aaebd60baeb94 Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Wed, 15 Jan 2025 09:17:20 -0700 Subject: [PATCH 1/3] Check each fork for supported precompiles. Contract compares gas cost for each precompile. Only checks a single unsupported precompile address. --- .../frontier/precompiles/test_precompiles.py | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/frontier/precompiles/test_precompiles.py diff --git a/tests/frontier/precompiles/test_precompiles.py b/tests/frontier/precompiles/test_precompiles.py new file mode 100644 index 0000000000..f49ca797c8 --- /dev/null +++ b/tests/frontier/precompiles/test_precompiles.py @@ -0,0 +1,103 @@ +"""Tests supported precompiled contracts.""" + +from typing import Iterator, Tuple + +import pytest + +from ethereum_test_forks import Fork +from ethereum_test_tools import ( + Account, + Alloc, + Environment, + StateTestFiller, + Transaction, +) +from ethereum_test_tools.code.generators import Conditional +from ethereum_test_tools.vm.opcode import Opcodes as Op + +UPPER_BOUND = 0x101 +NUM_UNSUPPORTED_PRECOMPILES = 1 + + +def precompile_addresses(fork: Fork) -> Iterator[Tuple[str, bool]]: + """ + Yield the addresses of precompiled contracts and their support status for a given fork. + + Args: + fork (Fork): The fork instance containing precompiled contract information. + + Yields: + Iterator[Tuple[str, bool]]: A tuple containing the address in hexadecimal format and a + boolean indicating whether the address is a supported precompile. + + """ + supported_precompiles = fork.precompiles() + + num_unsupported = NUM_UNSUPPORTED_PRECOMPILES + for address in range(1, UPPER_BOUND + 1): + if address in supported_precompiles: + yield (hex(address), True) + elif num_unsupported > 0: + # Check unsupported precompiles up to NUM_UNSUPPORTED_PRECOMPILES + yield (hex(address), False) + num_unsupported -= 1 + + +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize_by_fork("address,precompile_exists", precompile_addresses) +def test_precompiles( + state_test: StateTestFiller, address: str, precompile_exists: bool, pre: Alloc +): + """ + Tests the behavior of precompiled contracts in the Ethereum state test. + + Args: + state_test (StateTestFiller): The state test filler object used to run the test. + address (str): The address of the precompiled contract to test. + precompile_exists (bool): A flag indicating whether the precompiled contract exists at the + given address. + pre (Alloc): The allocation object used to deploy the contract and set up the initial + state. + + This test deploys a contract that performs two CALL operations to the specified address and a + fixed address (0x10000), measuring the gas used for each call. It then stores the difference + in gas usage in storage slot 0. The test verifies the expected storage value based on + whether the precompiled contract exists at the given address. + + """ + env = Environment() + + account = pre.deploy_contract( + Op.MSTORE(0x00, Op.GAS) + + Op.CALL(address=address) + + Op.MSTORE(0x00, Op.SUB(Op.MLOAD(0x00), Op.GAS)) + + Op.MSTORE(0x20, Op.GAS) + + Op.CALL(address=0x10000) + + Op.MSTORE(0x20, Op.SUB(Op.MLOAD(0x20), Op.GAS)) + + Op.SSTORE( + 0, + Op.LT( + Conditional( + condition=Op.GT(Op.MLOAD(0x00), Op.MLOAD(0x20)), + if_true=Op.SUB(Op.MLOAD(0x00), Op.MLOAD(0x20)), + if_false=Op.SUB(Op.MLOAD(0x20), Op.MLOAD(0x00)), + ), + 0x1A4, + ), + ) + + Op.STOP, + storage={0: 0xDEADBEEF}, + ) + + tx = Transaction( + to=account, + sender=pre.fund_eoa(), + gas_limit=1_000_000, + protected=True, + ) + + # A high gas cost will result from calling a precompile + # Expect 0x00 when a precompile exists at the address, 0x01 otherwise + post = {account: Account(storage={0: "0x00" if precompile_exists else "0x01"})} + + state_test(env=env, pre=pre, post=post, tx=tx) From 84fa66616daf1d3fd8ee534795c28661876a1c8d Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Tue, 11 Feb 2025 08:47:15 -0700 Subject: [PATCH 2/3] Include idPrecomps in converted-ethereum-tests.txt --- converted-ethereum-tests.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/converted-ethereum-tests.txt b/converted-ethereum-tests.txt index d19e3050e7..7ed7e7eb15 100644 --- a/converted-ethereum-tests.txt +++ b/converted-ethereum-tests.txt @@ -82,3 +82,6 @@ GeneralStateTests/VMTests/vmTests/push.json ([#1067](https://github.com/ethereum/execution-spec-tests/pull/1067)) GeneralStateTests/stPreCompiledContracts/blake2B.json + +([#1067](https://github.com/ethereum/execution-spec-tests/pull/1120)) +GeneralStateTests/stPreCompiledContracts/idPrecomps.json From f7d5f49ffac414b48a1206f751983e30241daea8 Mon Sep 17 00:00:00 2001 From: Stuart Reed Date: Wed, 12 Feb 2025 09:13:55 -0700 Subject: [PATCH 3/3] Use variables for memory slots --- .../frontier/precompiles/test_precompiles.py | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/tests/frontier/precompiles/test_precompiles.py b/tests/frontier/precompiles/test_precompiles.py index f49ca797c8..c5a100e14a 100644 --- a/tests/frontier/precompiles/test_precompiles.py +++ b/tests/frontier/precompiles/test_precompiles.py @@ -15,7 +15,7 @@ from ethereum_test_tools.code.generators import Conditional from ethereum_test_tools.vm.opcode import Opcodes as Op -UPPER_BOUND = 0x101 +UPPER_BOUND = 0xFF NUM_UNSUPPORTED_PRECOMPILES = 1 @@ -67,20 +67,45 @@ def test_precompiles( """ env = Environment() + slot_precompile_call_gas = 0x00 + slot_empty_address_call_gas = 0x20 + account = pre.deploy_contract( - Op.MSTORE(0x00, Op.GAS) - + Op.CALL(address=address) - + Op.MSTORE(0x00, Op.SUB(Op.MLOAD(0x00), Op.GAS)) - + Op.MSTORE(0x20, Op.GAS) - + Op.CALL(address=0x10000) - + Op.MSTORE(0x20, Op.SUB(Op.MLOAD(0x20), Op.GAS)) + Op.MSTORE(slot_precompile_call_gas, Op.GAS) + + Op.CALL( + address=address, + value=0, + args_offset=0, + args_size=32, + output_offset=32, + output_size=32, + ) + + Op.MSTORE(slot_precompile_call_gas, Op.SUB(Op.MLOAD(slot_precompile_call_gas), Op.GAS)) + + Op.MSTORE(slot_empty_address_call_gas, Op.GAS) + + Op.CALL( + address=0x10000, + value=0, + args_offset=0, + args_size=32, + output_offset=32, + output_size=32, + ) + + Op.MSTORE( + slot_empty_address_call_gas, Op.SUB(Op.MLOAD(slot_empty_address_call_gas), Op.GAS) + ) + Op.SSTORE( 0, Op.LT( Conditional( - condition=Op.GT(Op.MLOAD(0x00), Op.MLOAD(0x20)), - if_true=Op.SUB(Op.MLOAD(0x00), Op.MLOAD(0x20)), - if_false=Op.SUB(Op.MLOAD(0x20), Op.MLOAD(0x00)), + condition=Op.GT( + Op.MLOAD(slot_precompile_call_gas), Op.MLOAD(slot_empty_address_call_gas) + ), + if_true=Op.SUB( + Op.MLOAD(slot_precompile_call_gas), Op.MLOAD(slot_empty_address_call_gas) + ), + if_false=Op.SUB( + Op.MLOAD(slot_empty_address_call_gas), Op.MLOAD(slot_precompile_call_gas) + ), ), 0x1A4, ),