From 940cb3e7d334fb3533817ee78cb5a792bc1a641a Mon Sep 17 00:00:00 2001 From: Dimitry Kh Date: Mon, 28 Oct 2024 12:24:12 +0100 Subject: [PATCH] test scenarios --- converted-ethereum-tests.txt | 117 +++++ .../composite_types.py | 45 +- src/ethereum_test_forks/forks/forks.py | 2 +- src/ethereum_test_vm/opcode.py | 2 +- tests/frontier/scenarios/__init__.py | 3 + tests/frontier/scenarios/common.py | 294 +++++++++++ tests/frontier/scenarios/programs/__init__.py | 3 + .../programs/all_frontier_opcodes.py | 477 ++++++++++++++++++ .../scenarios/programs/context_calls.py | 244 +++++++++ .../scenarios/programs/invalid_opcodes.py | 50 ++ .../scenarios/programs/static_violation.py | 45 ++ .../frontier/scenarios/scenarios/__init__.py | 3 + .../scenarios/scenarios/call_combinations.py | 306 +++++++++++ .../scenarios/create_combinations.py | 178 +++++++ .../scenarios/revert_combinations.py | 48 ++ tests/frontier/scenarios/test_scenarios.py | 269 ++++++++++ whitelist.txt | 100 ++++ 17 files changed, 2174 insertions(+), 12 deletions(-) create mode 100644 tests/frontier/scenarios/__init__.py create mode 100644 tests/frontier/scenarios/common.py create mode 100644 tests/frontier/scenarios/programs/__init__.py create mode 100644 tests/frontier/scenarios/programs/all_frontier_opcodes.py create mode 100644 tests/frontier/scenarios/programs/context_calls.py create mode 100644 tests/frontier/scenarios/programs/invalid_opcodes.py create mode 100644 tests/frontier/scenarios/programs/static_violation.py create mode 100644 tests/frontier/scenarios/scenarios/__init__.py create mode 100644 tests/frontier/scenarios/scenarios/call_combinations.py create mode 100644 tests/frontier/scenarios/scenarios/create_combinations.py create mode 100644 tests/frontier/scenarios/scenarios/revert_combinations.py create mode 100644 tests/frontier/scenarios/test_scenarios.py diff --git a/converted-ethereum-tests.txt b/converted-ethereum-tests.txt index b42c9749c1..4babebb6a4 100644 --- a/converted-ethereum-tests.txt +++ b/converted-ethereum-tests.txt @@ -1,3 +1,120 @@ +([#808](https://github.com/ethereum/execution-spec-tests/pull/808)) +GeneralStateTests/stBadOpcode/invalidDiffPlaces.json +GeneralStateTests/stBadOpcode/opc0CDiffPlaces.json +GeneralStateTests/stBadOpcode/opc0DDiffPlaces.json +GeneralStateTests/stBadOpcode/opc0EDiffPlaces.json +GeneralStateTests/stBadOpcode/opc0FDiffPlaces.json +GeneralStateTests/stBadOpcode/opc1EDiffPlaces.json +GeneralStateTests/stBadOpcode/opc1FDiffPlaces.json +GeneralStateTests/stBadOpcode/opc2ADiffPlaces.json +GeneralStateTests/stBadOpcode/opc2BDiffPlaces.json +GeneralStateTests/stBadOpcode/opc2CDiffPlaces.json +GeneralStateTests/stBadOpcode/opc2DDiffPlaces.json +GeneralStateTests/stBadOpcode/opc2EDiffPlaces.json +GeneralStateTests/stBadOpcode/opc2FDiffPlaces.json +GeneralStateTests/stBadOpcode/opc4ADiffPlaces.json +GeneralStateTests/stBadOpcode/opc4BDiffPlaces.json +GeneralStateTests/stBadOpcode/opc4CDiffPlaces.json +GeneralStateTests/stBadOpcode/opc4DDiffPlaces.json +GeneralStateTests/stBadOpcode/opc4EDiffPlaces.json +GeneralStateTests/stBadOpcode/opc4FDiffPlaces.json +GeneralStateTests/stBadOpcode/opc5CDiffPlaces.json +GeneralStateTests/stBadOpcode/opc5DDiffPlaces.json +GeneralStateTests/stBadOpcode/opc5EDiffPlaces.json +GeneralStateTests/stBadOpcode/opc5FDiffPlaces.json +GeneralStateTests/stBadOpcode/opc21DiffPlaces.json +GeneralStateTests/stBadOpcode/opc22DiffPlaces.json +GeneralStateTests/stBadOpcode/opc23DiffPlaces.json +GeneralStateTests/stBadOpcode/opc24DiffPlaces.json +GeneralStateTests/stBadOpcode/opc25DiffPlaces.json +GeneralStateTests/stBadOpcode/opc26DiffPlaces.json +GeneralStateTests/stBadOpcode/opc27DiffPlaces.json +GeneralStateTests/stBadOpcode/opc28DiffPlaces.json +GeneralStateTests/stBadOpcode/opc29DiffPlaces.json +GeneralStateTests/stBadOpcode/opc49DiffPlaces.json +GeneralStateTests/stBadOpcode/opcA5DiffPlaces.json +GeneralStateTests/stBadOpcode/opcA6DiffPlaces.json +GeneralStateTests/stBadOpcode/opcA7DiffPlaces.json +GeneralStateTests/stBadOpcode/opcA8DiffPlaces.json +GeneralStateTests/stBadOpcode/opcA9DiffPlaces.json +GeneralStateTests/stBadOpcode/opcAADiffPlaces.json +GeneralStateTests/stBadOpcode/opcABDiffPlaces.json +GeneralStateTests/stBadOpcode/opcACDiffPlaces.json +GeneralStateTests/stBadOpcode/opcADDiffPlaces.json +GeneralStateTests/stBadOpcode/opcAEDiffPlaces.json +GeneralStateTests/stBadOpcode/opcAFDiffPlaces.json +GeneralStateTests/stBadOpcode/opcB0DiffPlaces.json +GeneralStateTests/stBadOpcode/opcB1DiffPlaces.json +GeneralStateTests/stBadOpcode/opcB2DiffPlaces.json +GeneralStateTests/stBadOpcode/opcB3DiffPlaces.json +GeneralStateTests/stBadOpcode/opcB4DiffPlaces.json +GeneralStateTests/stBadOpcode/opcB5DiffPlaces.json +GeneralStateTests/stBadOpcode/opcB6DiffPlaces.json +GeneralStateTests/stBadOpcode/opcB7DiffPlaces.json +GeneralStateTests/stBadOpcode/opcB8DiffPlaces.json +GeneralStateTests/stBadOpcode/opcB9DiffPlaces.json +GeneralStateTests/stBadOpcode/opcBADiffPlaces.json +GeneralStateTests/stBadOpcode/opcBBDiffPlaces.json +GeneralStateTests/stBadOpcode/opcBCDiffPlaces.json +GeneralStateTests/stBadOpcode/opcBDDiffPlaces.json +GeneralStateTests/stBadOpcode/opcBEDiffPlaces.json +GeneralStateTests/stBadOpcode/opcBFDiffPlaces.json +GeneralStateTests/stBadOpcode/opcC0DiffPlaces.json +GeneralStateTests/stBadOpcode/opcC1DiffPlaces.json +GeneralStateTests/stBadOpcode/opcC2DiffPlaces.json +GeneralStateTests/stBadOpcode/opcC3DiffPlaces.json +GeneralStateTests/stBadOpcode/opcC4DiffPlaces.json +GeneralStateTests/stBadOpcode/opcC5DiffPlaces.json +GeneralStateTests/stBadOpcode/opcC6DiffPlaces.json +GeneralStateTests/stBadOpcode/opcC7DiffPlaces.json +GeneralStateTests/stBadOpcode/opcC8DiffPlaces.json +GeneralStateTests/stBadOpcode/opcC9DiffPlaces.json +GeneralStateTests/stBadOpcode/opcCADiffPlaces.json +GeneralStateTests/stBadOpcode/opcCBDiffPlaces.json +GeneralStateTests/stBadOpcode/opcCCDiffPlaces.json +GeneralStateTests/stBadOpcode/opcCDDiffPlaces.json +GeneralStateTests/stBadOpcode/opcCEDiffPlaces.json +GeneralStateTests/stBadOpcode/opcCFDiffPlaces.json +GeneralStateTests/stBadOpcode/opcD0DiffPlaces.json +GeneralStateTests/stBadOpcode/opcD1DiffPlaces.json +GeneralStateTests/stBadOpcode/opcD2DiffPlaces.json +GeneralStateTests/stBadOpcode/opcD3DiffPlaces.json +GeneralStateTests/stBadOpcode/opcD4DiffPlaces.json +GeneralStateTests/stBadOpcode/opcD5DiffPlaces.json +GeneralStateTests/stBadOpcode/opcD6DiffPlaces.json +GeneralStateTests/stBadOpcode/opcD7DiffPlaces.json +GeneralStateTests/stBadOpcode/opcD8DiffPlaces.json +GeneralStateTests/stBadOpcode/opcD9DiffPlaces.json +GeneralStateTests/stBadOpcode/opcDADiffPlaces.json +GeneralStateTests/stBadOpcode/opcDBDiffPlaces.json +GeneralStateTests/stBadOpcode/opcDCDiffPlaces.json +GeneralStateTests/stBadOpcode/opcDDDiffPlaces.json +GeneralStateTests/stBadOpcode/opcDEDiffPlaces.json +GeneralStateTests/stBadOpcode/opcDFDiffPlaces.json +GeneralStateTests/stBadOpcode/opcE0DiffPlaces.json +GeneralStateTests/stBadOpcode/opcE1DiffPlaces.json +GeneralStateTests/stBadOpcode/opcE2DiffPlaces.json +GeneralStateTests/stBadOpcode/opcE3DiffPlaces.json +GeneralStateTests/stBadOpcode/opcE4DiffPlaces.json +GeneralStateTests/stBadOpcode/opcE5DiffPlaces.json +GeneralStateTests/stBadOpcode/opcE6DiffPlaces.json +GeneralStateTests/stBadOpcode/opcE7DiffPlaces.json +GeneralStateTests/stBadOpcode/opcE8DiffPlaces.json +GeneralStateTests/stBadOpcode/opcE9DiffPlaces.json +GeneralStateTests/stBadOpcode/opcEADiffPlaces.json +GeneralStateTests/stBadOpcode/opcEBDiffPlaces.json +GeneralStateTests/stBadOpcode/opcECDiffPlaces.json +GeneralStateTests/stBadOpcode/opcEDDiffPlaces.json +GeneralStateTests/stBadOpcode/opcEEDiffPlaces.json +GeneralStateTests/stBadOpcode/opcEFDiffPlaces.json +GeneralStateTests/stBadOpcode/opcF6DiffPlaces.json +GeneralStateTests/stBadOpcode/opcF7DiffPlaces.json +GeneralStateTests/stBadOpcode/opcF8DiffPlaces.json +GeneralStateTests/stBadOpcode/opcF9DiffPlaces.json +GeneralStateTests/stBadOpcode/opcFBDiffPlaces.json +GeneralStateTests/stBadOpcode/opcFCDiffPlaces.json +GeneralStateTests/stBadOpcode/opcFEDiffPlaces.json + ([#748](https://github.com/ethereum/execution-spec-tests/pull/748)) GeneralStateTests/stBadOpcode/badOpcodes.json GeneralStateTests/stBugs/evmBytecode.json diff --git a/src/ethereum_test_base_types/composite_types.py b/src/ethereum_test_base_types/composite_types.py index 7627c5b107..0b9a31f847 100644 --- a/src/ethereum_test_base_types/composite_types.py +++ b/src/ethereum_test_base_types/composite_types.py @@ -1,6 +1,7 @@ """ Base composite types for Ethereum test cases. """ + from dataclasses import dataclass from typing import Any, ClassVar, Dict, SupportsBytes, Type, TypeAlias @@ -24,6 +25,7 @@ class Storage(RootModel[Dict[StorageKeyValueType, StorageKeyValueType]]): root: Dict[StorageKeyValueType, StorageKeyValueType] = Field(default_factory=dict) _current_slot: int = PrivateAttr(0) + _hint_map: Dict[StorageKeyValueType, str] = {} StorageDictType: ClassVar[TypeAlias] = Dict[ str | int | bytes | SupportsBytes, str | int | bytes | SupportsBytes @@ -92,13 +94,15 @@ class KeyValueMismatch(Exception): key: int want: int got: int + hint: str - def __init__(self, address: Address, key: int, want: int, got: int, *args): + def __init__(self, address: Address, key: int, want: int, got: int, hint: str = "", *args): super().__init__(args) self.address = address self.key = key self.want = want self.got = got + self.hint = hint def __str__(self): """Print exception string""" @@ -107,7 +111,7 @@ def __str__(self): label_str = f" ({self.address.label})" return ( f"incorrect value in address {self.address}{label_str} for " - + f"key {Hash(self.key)}:" + + f"key {Hash(self.key)} ({self.hint}):" + f" want {HexNumber(self.want)} (dec:{int(self.want)})," + f" got {HexNumber(self.got)} (dec:{int(self.got)})" ) @@ -128,9 +132,9 @@ def __setitem__( value: StorageKeyValueTypeConvertible | StorageKeyValueType, ): # noqa: SC200 """Sets an item in the storage""" - self.root[ - StorageKeyValueTypeAdapter.validate_python(key) - ] = StorageKeyValueTypeAdapter.validate_python(value) + self.root[StorageKeyValueTypeAdapter.validate_python(key)] = ( + StorageKeyValueTypeAdapter.validate_python(value) + ) def __delitem__(self, key: StorageKeyValueTypeConvertible | StorageKeyValueType): """Deletes an item from the storage""" @@ -182,7 +186,7 @@ def items(self): return self.root.items() def store_next( - self, value: StorageKeyValueTypeConvertible | StorageKeyValueType | bool + self, value: StorageKeyValueTypeConvertible | StorageKeyValueType | bool, hint: str = "" ) -> StorageKeyValueType: """ Stores a value in the storage and returns the key where the value is stored. @@ -192,6 +196,7 @@ def store_next( """ slot = StorageKeyValueTypeAdapter.validate_python(self._current_slot) self._current_slot += 1 + self._hint_map[slot] = hint self[slot] = StorageKeyValueTypeAdapter.validate_python(value) return slot @@ -230,7 +235,11 @@ def must_contain(self, address: Address, other: "Storage"): raise Storage.MissingKey(key=key) elif self[key] != other[key]: raise Storage.KeyValueMismatch( - address=address, key=key, want=self[key], got=other[key] + address=address, + key=key, + want=self[key], + got=other[key], + hint=self._hint_map.get(key, ""), ) def must_be_equal(self, address: Address, other: "Storage | None"): @@ -243,17 +252,33 @@ def must_be_equal(self, address: Address, other: "Storage | None"): for key in self.keys() & other.keys(): if self[key] != other[key]: raise Storage.KeyValueMismatch( - address=address, key=key, want=self[key], got=other[key] + address=address, + key=key, + want=self[key], + got=other[key], + hint=self._hint_map.get(key, ""), ) # Test keys contained in either one of the storage objects for key in self.keys() ^ other.keys(): if key in self: if self[key] != 0: - raise Storage.KeyValueMismatch(address=address, key=key, want=self[key], got=0) + raise Storage.KeyValueMismatch( + address=address, + key=key, + want=self[key], + got=0, + hint=self._hint_map.get(key, ""), + ) elif other[key] != 0: - raise Storage.KeyValueMismatch(address=address, key=key, want=0, got=other[key]) + raise Storage.KeyValueMismatch( + address=address, + key=key, + want=0, + got=other[key], + hint=self._hint_map.get(key, ""), + ) def canary(self) -> "Storage": """ diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 6c20c84f67..d065a276ed 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -468,7 +468,7 @@ def valid_opcodes( """ Returns the list of Opcodes that are valid to work on this fork. """ - return [Opcodes.RETURNDATASIZE, Opcodes.STATICCALL] + super(Byzantium, cls).valid_opcodes() + return [Opcodes.REVERT, Opcodes.RETURNDATASIZE, Opcodes.STATICCALL] + super(Byzantium, cls).valid_opcodes() class Constantinople(Byzantium): diff --git a/src/ethereum_test_vm/opcode.py b/src/ethereum_test_vm/opcode.py index 195c93809b..cfad0240e2 100644 --- a/src/ethereum_test_vm/opcode.py +++ b/src/ethereum_test_vm/opcode.py @@ -326,7 +326,7 @@ def __new__( instance.lambda_operation = lambda_operation return instance - def __call__(self, *args_t: OpcodeCallArg) -> Bytecode: + def __call__(self, *args_t: OpcodeCallArg, **kwargs) -> Bytecode: """ Performs the macro operation if any. Otherwise is a no-op. diff --git a/tests/frontier/scenarios/__init__.py b/tests/frontier/scenarios/__init__.py new file mode 100644 index 0000000000..20f2893c0e --- /dev/null +++ b/tests/frontier/scenarios/__init__.py @@ -0,0 +1,3 @@ +""" +Scenarios common import +""" diff --git a/tests/frontier/scenarios/common.py b/tests/frontier/scenarios/common.py new file mode 100644 index 0000000000..86e2529a3b --- /dev/null +++ b/tests/frontier/scenarios/common.py @@ -0,0 +1,294 @@ +""" +Define Scenario structures and helpers for test_scenarios test +""" + +from dataclasses import dataclass +from enum import Enum +from typing import List, Tuple + +from _pytest.mark import ParameterSet + +from ethereum_test_base_types import to_bytes +from ethereum_test_forks import Fork, Frontier +from ethereum_test_tools import Address, Alloc, Bytecode, Conditional, Hash +from ethereum_test_tools.vm.opcode import Opcodes as Op + + +@dataclass +class ScenarioDebug: + """Debug selector for the development""" + + test_param: ParameterSet | None + scenario_name: str + + +class ScenarioExpectOpcode(Enum): + """Opcodes that are replaced to real values computed by the scenario""" + + TX_ORIGIN = 1 + CODE_ADDRESS = 2 + CODE_CALLER = 3 + SELFBALANCE = 4 + BALANCE = 5 + CALL_VALUE = 6 + CALL_DATALOAD_0 = 7 + CALL_DATASIZE = 8 + GASPRICE = 9 + BLOCKHASH_0 = 10 + COINBASE = 11 + TIMESTAMP = 12 + NUMBER = 13 + DIFFICULTY_RANDAO = 14 + GASLIMIT = 15 + BASEFEE = 16 + BLOBHASH_0 = 17 + BLOBBASEFEE = 18 + + +@dataclass +class ProgramResult: + """ + Describe expected result of a program + + Attributes: + result (int | ScenarioExpectOpcode): The result of the program + from_fork (Fork): The result is only valid from this fork (default: Frontier) + static_support (bool): Can be verified in static context (default: True) + """ + + result: int | ScenarioExpectOpcode + + """The result is only valid from this fork""" + from_fork: Fork = Frontier + static_support: bool = True + + +@dataclass +class ScenarioEnvironment: + """ + Scenario evm environment + Each scenario must define an environment on which program is executed + This is so post state verification could check results of evm opcodes + """ + + code_address: Address # Op.ADDRESS, address scope for program + code_caller: Address # Op.CALLER, caller of the program + selfbalance: int # Op.SELFBALANCE, balance of the environment of the program + ext_balance: int # Op.BALANCE(external) of fixed address deployed in state + call_value: int # Op.CALLVALUE of call that is done to the program + call_dataload_0: int # Op.CALLDATALOAD(0) expected result + call_datasize: int # Op.CALLDATASIZE expected result + has_static: bool = False # Weather scenario execution context is static + + +@dataclass +class ExecutionEnvironment: + """ + Scenario execution environment which is determined by test + """ + + fork: Fork + gasprice: int + origin: Address + coinbase: Address + blockhash_0: Hash + timestamp: int + number: int + difficulty_randao: int + gaslimit: int + basefee: int + blobhash_0: Hash + blob_basefee: int + + +@dataclass +class ScenarioGeneratorInput: + """ + Parameters for the scenario generator function + + Attributes: + fork (Fork): Fork for which we ask to generate scenarios + pre (Alloc): Access to the state to be able to deploy contracts into pre + operation (Bytecode): Evm bytecode program that will be tested + external_address (Address): Static external address for ext opcodes + """ + + fork: Fork + pre: Alloc + operation_code: Bytecode + external_address: Address + external_balance: int + + +@dataclass +class Scenario: + """ + Describe test scenario that will be run in test for each program + + Attributes: + name (str): Scenario name for the test vector + code (Address): Address that is an entry point for scenario code + env (ScenarioEnvironment): Evm values for ScenarioExpectAddress map + reverting (bool): If scenario reverts program execution, making result 0 (default: False) + """ + + name: str + code: Address + env: ScenarioEnvironment + halts: bool = False + + +def translate_result( + res: ProgramResult, env: ScenarioEnvironment, exec_env: ExecutionEnvironment +) -> int: + """ + Translate expected program result code into concrete value, + given the scenario evm environment and test execution environment + """ + if exec_env.fork < res.from_fork: + return 0 + if not res.static_support and env.has_static: + return 0 + if isinstance(res.result, ScenarioExpectOpcode): + if res.result == ScenarioExpectOpcode.TX_ORIGIN: + return int(exec_env.origin.hex(), 16) + if res.result == ScenarioExpectOpcode.CODE_ADDRESS: + return int(env.code_address.hex(), 16) + if res.result == ScenarioExpectOpcode.CODE_CALLER: + return int(env.code_caller.hex(), 16) + if res.result == ScenarioExpectOpcode.BALANCE: + return int(env.ext_balance) + if res.result == ScenarioExpectOpcode.CALL_VALUE: + return int(env.call_value) + if res.result == ScenarioExpectOpcode.CALL_DATALOAD_0: + return env.call_dataload_0 + if res.result == ScenarioExpectOpcode.CALL_DATASIZE: + return env.call_datasize + if res.result == ScenarioExpectOpcode.GASPRICE: + return exec_env.gasprice + if res.result == ScenarioExpectOpcode.BLOCKHASH_0: + return int(exec_env.blockhash_0.hex(), 16) + if res.result == ScenarioExpectOpcode.COINBASE: + return int(exec_env.coinbase.hex(), 16) + if res.result == ScenarioExpectOpcode.TIMESTAMP: + return exec_env.timestamp + if res.result == ScenarioExpectOpcode.NUMBER: + return exec_env.number + if res.result == ScenarioExpectOpcode.DIFFICULTY_RANDAO: + return exec_env.difficulty_randao + if res.result == ScenarioExpectOpcode.GASLIMIT: + return exec_env.gaslimit + if res.result == ScenarioExpectOpcode.SELFBALANCE: + return int(env.selfbalance) + if res.result == ScenarioExpectOpcode.BASEFEE: + return exec_env.basefee + if res.result == ScenarioExpectOpcode.BLOBHASH_0: + return int(exec_env.blobhash_0.hex(), 16) + if res.result == ScenarioExpectOpcode.BLOBBASEFEE: + return exec_env.blob_basefee + + return res.result + + +def replace_special_calls_in_operation( + pre: Alloc, operation: Bytecode, external_address: Address +) -> Bytecode: + """ + Run find replace of some special calls to the contracts that we don't know at compile time + replace 0xfff..fff address to external_address + replace special call to 0xfff..ffe address to gas_hash_address contract + """ + gas_hash_address = make_gas_hash_contract(pre) + invalid_opcode_contract = make_invalid_opcode_contract(pre) + + """Replace Op.CALL(Op.GAS, 0xfff..ffe, 0, 64, 32, 0, 0) to gas_hash_address""" + """Replace Op.CALL(1000, 0xfff..ffd, 0, 64, 32, 0, 0) to invalid_opcode_contract""" + """Replace BALANCE(0xfff..fff) to BALANCE(external_address) in operation""" + """Replace EXTCODESIZE(0xfff..fff) to EXTCODESIZE(external_address) in operation""" + """Replace EXTCODEHASH(0xfff..fff) to EXTCODEHASH(external_address) in operation""" + """Replace EXTCODECOPY(0xfff..fff, ...) to EXTCODECOPY(external_address, ...)""" + replace_list: List[Tuple[str, str]] = [ + ( + "600060006020604060007ffffffffffffffffffffffffffffffffffffffffffffffff" + "ffffffffffffffffd620186a0f1", + Op.CALL(10000, invalid_opcode_contract, 0, 64, 32, 0, 0).hex(), + ), + ( + "600060006020604060007ffffffffffffffffffffffffffffffffffffffffffffffff" + "ffffffffffffffffe5af1", + Op.CALL(Op.GAS, gas_hash_address, 0, 64, 32, 0, 0).hex(), + ), + ( + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff31", + Op.BALANCE(external_address).hex(), + ), + ( + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3b", + Op.EXTCODESIZE(external_address).hex(), + ), + ( + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f", + Op.EXTCODEHASH(external_address).hex(), + ), + ( + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3c", + "3c".join(Op.BALANCE(external_address).hex().rsplit("31", 1)), + ), + ] + for find, replace in replace_list: + if find in operation.hex(): + new_operation_hex = operation.hex().replace( + find, + replace, + ) + operation = Bytecode( + bytes_or_byte_code_base=to_bytes(new_operation_hex), + popped_stack_items=0, + pushed_stack_items=0, + ) + return operation + + +def make_gas_hash_contract(pre: Alloc) -> Address: + """ + Contract that spends unique amount of gas based on input + Used for the values we can't predict, can be gas consuming on high values + So that if we can't check exact value in expect section, + we at least could spend unique gas amount + """ + gas_hash_address = pre.deploy_contract( + code=Op.MSTORE(0, 0) + + Op.JUMPDEST + + Op.CALLDATACOPY(63, Op.MLOAD(0), 1) + + Op.JUMPDEST + + Conditional( + condition=Op.EQ(Op.MLOAD(32), 0), + if_true=Op.MSTORE(0, Op.ADD(1, Op.MLOAD(0))) + + Conditional( + condition=Op.GT(Op.MLOAD(0), 32), + if_true=Op.RETURN(0, 0), + if_false=Op.JUMP(5), + ), + if_false=Op.MSTORE(32, Op.SUB(Op.MLOAD(32), 1)) + Op.JUMP(14), + ) + ) + return gas_hash_address + + +def make_invalid_opcode_contract(pre: Alloc) -> Address: + """ + Deploy a contract that will execute any asked byte as an opcode from calldataload + With 0-ed input stack of 10 elements, valid for opcodes starting at 0x0C + """ + invalid_opcode_caller = pre.deploy_contract( + code=Op.PUSH0 * 10 + + Op.JUMP(Op.SUB(Op.MUL(2, Op.CALLDATALOAD(0)), 1)) # here pc is 19 + + Op.JUMPDEST * 4 + + sum( + [ + Bytecode(bytes([opcode]), popped_stack_items=0, pushed_stack_items=0) + Op.JUMPDEST + for opcode in range(0x0C, 0xFF) + ], + ) + ) + return invalid_opcode_caller diff --git a/tests/frontier/scenarios/programs/__init__.py b/tests/frontier/scenarios/programs/__init__.py new file mode 100644 index 0000000000..20f2893c0e --- /dev/null +++ b/tests/frontier/scenarios/programs/__init__.py @@ -0,0 +1,3 @@ +""" +Scenarios common import +""" diff --git a/tests/frontier/scenarios/programs/all_frontier_opcodes.py b/tests/frontier/scenarios/programs/all_frontier_opcodes.py new file mode 100644 index 0000000000..cd94bc14af --- /dev/null +++ b/tests/frontier/scenarios/programs/all_frontier_opcodes.py @@ -0,0 +1,477 @@ +""" +Define a program for scenario test that executes all frontier opcodes and entangles it's result +""" + +import pytest + +from ethereum_test_tools import Bytecode, Conditional +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from ..common import ProgramResult + +# Opcodes that are not in Frontier +# 1b - SHL +# 1c - SHR +# 1d - SAR + + +def make_all_opcode_program() -> Bytecode: + """Make a program that call each Frontier opcode and verifies it's result""" + code: Bytecode = ( + # Test opcode 01 - ADD + Conditional( + condition=Op.EQ(Op.ADD(1, 1), 2), + if_true=Op.MSTORE(0, 2), + if_false=Op.MSTORE(0, 0) + Op.RETURN(0, 32), + ) + # Test opcode 02 - MUL + + Conditional( + condition=Op.EQ( + Op.MUL(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 2), + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE, + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 2) + Op.RETURN(0, 32), + ) + # Test 03 - SUB + + Conditional( + condition=Op.EQ( + Op.SUB(0, 1), + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 3) + Op.RETURN(0, 32), + ) + # Test 04 - DIV + + Conditional( + condition=Op.AND(Op.EQ(Op.DIV(1, 2), 0), Op.EQ(Op.DIV(10, 2), 5)), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 4) + Op.RETURN(0, 32), + ) + # Test 05 - SDIV + + Conditional( + condition=Op.EQ( + Op.SDIV( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + ), + 2, + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 5) + Op.RETURN(0, 32), + ) + # Test 06 - MOD + + Conditional( + condition=Op.AND( + Op.EQ(Op.MOD(10, 3), 1), + Op.EQ( + Op.MOD( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD, + ), + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 6) + Op.RETURN(0, 32), + ) + # Test 07 - SMOD + + Conditional( + condition=Op.AND( + Op.EQ(Op.SMOD(10, 3), 1), + Op.EQ( + Op.SMOD( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD, + ), + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 7) + Op.RETURN(0, 32), + ) + # Test 08 - ADDMOD + + Conditional( + condition=Op.AND( + Op.EQ(Op.ADDMOD(10, 10, 8), 4), + Op.EQ( + Op.ADDMOD( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + 2, + 2, + ), + 1, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 8) + Op.RETURN(0, 32), + ) + # Test 09 - MULMOD + + Conditional( + condition=Op.AND( + Op.EQ(Op.MULMOD(10, 10, 8), 4), + Op.EQ( + Op.MULMOD( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + 12, + ), + 9, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 9) + Op.RETURN(0, 32), + ) + # Test 0A - EXP + + Conditional( + condition=Op.AND( + Op.EQ(Op.EXP(10, 2), 100), + Op.EQ( + Op.EXP(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD, 2), + 9, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 10) + Op.RETURN(0, 32), + ) + # Test 0B - SIGNEXTEND + + Conditional( + condition=Op.AND( + Op.EQ( + Op.SIGNEXTEND(0, 0xFF), + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + ), + Op.EQ( + Op.SIGNEXTEND(0, 0x7F), + 0x7F, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 11) + Op.RETURN(0, 32), + ) + # Test 10 - LT + + Conditional( + condition=Op.AND( + Op.EQ( + Op.LT(9, 10), + 1, + ), + Op.EQ( + Op.LT(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 0), + 0, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x10) + Op.RETURN(0, 32), + ) + # Test 11 - GT + + Conditional( + condition=Op.AND( + Op.EQ( + Op.GT(9, 10), + 0, + ), + Op.EQ( + Op.GT(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 0), + 1, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x11) + Op.RETURN(0, 32), + ) + # Test 12 - SLT + + Conditional( + condition=Op.AND( + Op.EQ( + Op.SLT(9, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF), + 0, + ), + Op.EQ( + Op.SLT(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 0), + 1, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x12) + Op.RETURN(0, 32), + ) + # Test 13 - SGT + + Conditional( + condition=Op.AND( + Op.EQ( + Op.SGT(10, 10), + 0, + ), + Op.EQ( + Op.SGT(0, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF), + 1, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x13) + Op.RETURN(0, 32), + ) + # Test 14 - EQ Skip + # Test 15 - ISZero + + Conditional( + condition=Op.AND( + Op.EQ(Op.ISZERO(10), 0), + Op.EQ(Op.ISZERO(0), 1), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x15) + Op.RETURN(0, 32), + ) + # Test 16 - AND Skip + # Test 17 - OR + + Conditional( + condition=Op.AND( + Op.EQ(Op.OR(0xF0, 0xF), 0xFF), + Op.EQ(Op.OR(0xFF, 0xFF), 0xFF), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x17) + Op.RETURN(0, 32), + ) + # Test 18 - XOR + + Conditional( + condition=Op.AND( + Op.EQ(Op.XOR(0xF0, 0xF), 0xFF), + Op.EQ(Op.XOR(0xFF, 0xFF), 0), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x18) + Op.RETURN(0, 32), + ) + # Test 19 - NOT + + Conditional( + condition=Op.AND( + Op.EQ( + Op.NOT(0), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + ), + Op.EQ( + Op.NOT(0xFFFFFFFFFFFF), + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000, + ), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x19) + Op.RETURN(0, 32), + ) + # Test 1A - BYTE + + Conditional( + condition=Op.AND( + Op.EQ(Op.BYTE(31, 0xFF), 0xFF), + Op.EQ(Op.BYTE(30, 0xFF00), 0xFF), + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x1A) + Op.RETURN(0, 32), + ) + # Test 20 - SHA3 + + Op.MSTORE(0, 0xFFFFFFFF) + + Conditional( + condition=Op.EQ( + Op.SHA3(28, 4), + 0x29045A592007D0C246EF02C2223570DA9522D0CF0F73282C79A1BC8F0BB2C238, + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x20) + Op.RETURN(0, 32), + ) + # 50 POP + # 51 MLOAD + # 52 MSTORE + # 53 MSTORE8 + + Op.MSTORE(0, 0) + + Op.MSTORE8(0, 0xFFFF) + + Conditional( + condition=Op.EQ( + Op.MLOAD(0), + 0xFF00000000000000000000000000000000000000000000000000000000000000, + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x53) + Op.RETURN(0, 32), + ) + # 54 SLOAD + + Conditional( + condition=Op.EQ(Op.SLOAD(0), 0), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x54) + Op.RETURN(0, 32), + ) + # 55 SSTORE # can't use because of static contexts + # 56 JUMP + # 57 JUMPI + # 58 PC + + Conditional( + condition=Op.EQ(Op.PC, 1660), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x58) + Op.RETURN(0, 32), + ) + # 59 MSIZE + + Op.MSTORE(64, 123) + + Conditional( + condition=Op.EQ(Op.MSIZE, 96), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x59) + Op.RETURN(0, 32), + ) + # 5A GAS + # 5B JUMPDEST + # 5C TLOAD + # 5D TSTORE # can't use because of static contexts + # 5E MCOPY + # 5F PUSH0 + # 60 - 7F PUSH X + + Op.PUSH1(0xFF) + + Op.PUSH2(0xFFFF) + + Op.ADD + + Op.PUSH3(0xFFFFFF) + + Op.ADD + + Op.PUSH4(0xFFFFFFFF) + + Op.ADD + + Op.PUSH5(0xFFFFFFFFFF) + + Op.ADD + + Op.PUSH6(0xFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH7(0xFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH8(0xFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH9(0xFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH10(0xFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH11(0xFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH12(0xFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH13(0xFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH14(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH15(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH16(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH17(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH18(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH19(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH20(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH21(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH22(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH23(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH24(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH25(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH26(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH27(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH28(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH29(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH30(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH31(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH32(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + + Op.ADD + + Op.PUSH1(0) + + Op.MSTORE + + Conditional( + condition=Op.EQ( + Op.MLOAD(0), 0x1010101010101010101010101010101010101010101010101010101010100E0 + ), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 60) + Op.RETURN(0, 32), + ) + # 80 - 8F DUP X + + Op.PUSH1(1) + + Op.DUP1 + + Op.DUP2 + + Op.DUP3 + + Op.DUP4 + + Op.DUP5 + + Op.DUP6 + + Op.DUP7 + + Op.DUP8 + + Op.DUP9 + + Op.DUP10 + + Op.DUP11 + + Op.DUP12 + + Op.DUP13 + + Op.DUP14 + + Op.DUP15 + + Op.DUP16 + + Op.ADD * 16 + + Op.PUSH1(0) + + Op.MSTORE + + Conditional( + condition=Op.EQ(Op.MLOAD(0), 17), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x80) + Op.RETURN(0, 32), + ) + # 90 - 9F SWAP X + + Op.PUSH1(3) + + Op.PUSH1(5) + + Op.SWAP1 + + Op.PUSH1(7) + + Op.SWAP2 + + Op.PUSH1(11) + + Op.SWAP3 + + Op.PUSH1(13) + + Op.SWAP4 + + Op.PUSH1(17) + + Op.SWAP5 + + Op.PUSH1(19) + + Op.SWAP6 + + Op.PUSH1(23) + + Op.SWAP7 + + Op.PUSH1(29) + + Op.SWAP8 + + Op.PUSH1(31) + + Op.SWAP9 + + Op.PUSH1(37) + + Op.SWAP10 + + Op.PUSH1(41) + + Op.SWAP11 + + Op.PUSH1(43) + + Op.SWAP12 + + Op.PUSH1(47) + + Op.SWAP13 + + Op.PUSH1(53) + + Op.SWAP14 + + Op.PUSH1(59) + + Op.SWAP15 + + Op.PUSH1(61) + + Op.SWAP16 + + Op.PUSH1(0) + + Op.MSTORE + + Conditional( + condition=Op.EQ(Op.MLOAD(0), 59), + if_true=Op.JUMPDEST, + if_false=Op.MSTORE(0, 0x90) + Op.RETURN(0, 32), + ) + # A0 - A4 LOG X - can't use because non static + # F0 CREATE + # F1 CALL + # F2 CALLCODE + # F3 RETURN + # F4 DELEGATECALL + # F5 CREATE2 + # FA STATICCALL + # FD REVERT + # FE INVALID + # FF SELFDESTRUCT + + Op.MSTORE(0, 1) + + Op.RETURN(0, 32) + ) + return code + + +program_all_frontier_opcodes = pytest.param( + make_all_opcode_program(), + ProgramResult(result=1), + id="program_ALL_FRONTIER_OPCODES", +) diff --git a/tests/frontier/scenarios/programs/context_calls.py b/tests/frontier/scenarios/programs/context_calls.py new file mode 100644 index 0000000000..17c042fa20 --- /dev/null +++ b/tests/frontier/scenarios/programs/context_calls.py @@ -0,0 +1,244 @@ +""" +Define programs that will run all context opcodes for test scenarios +""" + +import pytest + +from ethereum_test_forks import Byzantium, Cancun, Constantinople, Istanbul, London, Shanghai +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from ..common import ProgramResult, ScenarioExpectOpcode + +# Check that ADDRESS is really the code execution address in all scenarios +program_address = pytest.param( + Op.MSTORE(0, Op.ADDRESS) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.CODE_ADDRESS), + id="program_ADDRESS", +) + +# Check the BALANCE in all execution contexts +program_balance = pytest.param( + Op.MSTORE(0, Op.BALANCE(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) + + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.BALANCE), + id="program_BALANCE", +) + +# Check that ORIGIN stays the same in all contexts +program_origin = pytest.param( + Op.MSTORE(0, Op.ORIGIN) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.TX_ORIGIN), + id="program_ORIGIN", +) + + +# Check the CALLER in all execution contexts +program_caller = pytest.param( + Op.MSTORE(0, Op.CALLER) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.CODE_CALLER), + id="program_CALLER", +) + +# Check the CALLVALUE in all execution contexts +program_callvalue = pytest.param( + Op.MSTORE(0, Op.CALLVALUE) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.CALL_VALUE), + id="program_CALLVALUE", +) + +# Check the CALLDATALOAD in all execution contexts +program_calldataload = pytest.param( + Op.MSTORE(0, Op.CALLDATALOAD(0)) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.CALL_DATALOAD_0), + id="program_CALLDATALOAD", +) + +# Check the CALLDATASIZE in all execution contexts +program_calldatasize = pytest.param( + Op.MSTORE(0, Op.CALLDATASIZE) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.CALL_DATASIZE), + id="program_CALLDATASIZE", +) + +# Check the CALLDATACOPY in all execution contexts +program_calldatacopy = pytest.param( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.CALL_DATALOAD_0), + id="program_CALLDATACOPY", +) + +# Check that codecopy and codesize stays the same in all contexts +program_codecopy_codesize = pytest.param( + Op.MSTORE(0, Op.CODESIZE) + Op.CODECOPY(0, 0, 30) + Op.RETURN(0, 32), + ProgramResult(result=0x38600052601E600060003960206000F300000000000000000000000000000010), + id="program_CODECOPY_CODESIZE", +) + +# Check that gasprice stays the same in all contexts +program_gasprice = pytest.param( + Op.MSTORE(0, Op.GASPRICE) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.GASPRICE), + id="program_GASPRICE", +) + +# Check that extcodesize and extcodecopy of pre deployed contract stay the same in all contexts +program_ext_codecopy_codesize = pytest.param( + Op.MSTORE( + 0, Op.EXTCODESIZE(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + ) + + Op.EXTCODECOPY(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 0, 0, 30) + + Op.RETURN(0, 32), + ProgramResult(result=0x6001600101000000000000000000000000000000000000000000000000000005), + id="program_EXTCODECOPY_EXTCODESIZE", +) + +# Check that returndatasize stays the same in all contexts +program_returndatasize = pytest.param( + Op.MSTORE(0, Op.RETURNDATASIZE) + + Op.CALL(100000, 2, 0, 0, 10, 32, 20) + + Op.MSTORE(0, Op.ADD(Op.MLOAD(0), Op.RETURNDATASIZE)) + + Op.RETURN(0, 32), + ProgramResult(result=32, from_fork=Byzantium), + id="program_RETURNDATASIZE", +) + +# Check that returndatacopy stays the same in all contexts +program_returndatacopy = pytest.param( + Op.CALL(100000, 2, 0, 0, 10, 32, 20) + Op.RETURNDATACOPY(0, 0, 32) + Op.RETURN(0, 32), + ProgramResult( + result=0x1D448AFD928065458CF670B60F5A594D735AF0172C8D67F22A81680132681CA, + from_fork=Byzantium, + ), + id="program_RETURNDATACOPY", +) + +# Check that extcodehash stays the same in all contexts +program_extcodehash = pytest.param( + Op.MSTORE( + 0, Op.EXTCODEHASH(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + ) + + Op.RETURN(0, 32), + ProgramResult( + result=0x8C634A8B28DD46F5DCB9A9F5DA1FAED26D0FB5ED98F3873A29AD27AAAFFDE0E4, + from_fork=Constantinople, + ), + id="program_EXTCODEHASH", +) + +# Check that blockhash stays the same in all contexts +# Need a way to pre calculate at least hash of block 0 +program_blockhash = pytest.param( + # Calculate gas hash of Op.BLOCKHASH(0) value + Op.MSTORE(64, Op.BLOCKHASH(0)) + + Op.CALL( + Op.GAS, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE, 0, 64, 32, 0, 0 + ) + + Op.MSTORE(32, Op.BLOCKHASH(0)) + + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.BLOCKHASH_0), + id="program_BLOCKHASH", +) + +# Need a way to pre calculate coinbase +program_coinbase = pytest.param( + Op.MSTORE(0, Op.COINBASE) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.COINBASE), + id="program_COINBASE", +) + +program_timestamp = pytest.param( + Op.MSTORE(0, Op.TIMESTAMP) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.TIMESTAMP), + id="program_TIMESTAMP", +) + +program_number = pytest.param( + Op.MSTORE(0, Op.NUMBER) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.NUMBER), + id="program_NUMBER", +) + +program_difficulty_randao = pytest.param( + # Calculate gas hash of DIFFICULTY value + Op.MSTORE(64, Op.PREVRANDAO) + + Op.CALL( + Op.GAS, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE, 0, 64, 32, 0, 0 + ) + + Op.MSTORE(32, Op.PREVRANDAO) + + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.DIFFICULTY_RANDAO), + id="program_DIFFICULTY", +) + +program_gaslimit = pytest.param( + Op.MSTORE(0, Op.GASLIMIT) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.GASLIMIT), + id="program_GASLIMIT", +) + +# Check that chainid stays the same in all contexts +program_chainid = pytest.param( + Op.MSTORE(0, Op.CHAINID) + Op.RETURN(0, 32), + ProgramResult(result=1, from_fork=Istanbul), + id="program_CHAINID", +) + +# Check the SELFBALANCE in all execution contexts +program_selfbalance = pytest.param( + Op.MSTORE(0, Op.SELFBALANCE) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.SELFBALANCE, from_fork=Istanbul), + id="program_SELFBALANCE", +) + +program_basefee = pytest.param( + # Calculate gas hash of BASEFEE value + Op.MSTORE(64, Op.BASEFEE) + + Op.CALL( + Op.GAS, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE, 0, 64, 32, 0, 0 + ) + + Op.MSTORE(32, Op.BASEFEE) + + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.BASEFEE, from_fork=London), + id="program_BASEFEE", +) + +program_blobhash = pytest.param( + Op.MSTORE(0, Op.BLOBHASH(0)) + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.BLOBHASH_0, from_fork=Cancun), + id="program_BLOBHASH", +) + +program_blobbasefee = pytest.param( + # Calculate gas hash of BLOBBASEFEE value + Op.MSTORE(64, Op.BLOBBASEFEE) + + Op.CALL( + Op.GAS, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE, 0, 64, 32, 0, 0 + ) + + Op.MSTORE(32, Op.BLOBBASEFEE) + + Op.RETURN(0, 32), + ProgramResult(result=ScenarioExpectOpcode.BLOBBASEFEE, from_fork=Cancun), + id="program_BLOBBASEFEE", +) + +program_tload = pytest.param( + Op.MSTORE(0, Op.TLOAD(0)) + Op.RETURN(0, 32), + ProgramResult(result=0, from_fork=Cancun), + id="program_TLOAD", +) + +program_mcopy = pytest.param( + Op.MSTORE(0, 0) + + Op.MSTORE(32, 0x000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F) + + Op.MCOPY(0, 32, 32) + + Op.RETURN(0, 32), + ProgramResult( + result=0x000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F, from_fork=Cancun + ), + id="program_MCOPY", +) + +program_push0 = pytest.param( + Op.PUSH1(10) + Op.PUSH0 + Op.MSTORE + Op.RETURN(0, 32), + ProgramResult(result=10, from_fork=Shanghai), + id="program_PUSH0", +) diff --git a/tests/frontier/scenarios/programs/invalid_opcodes.py b/tests/frontier/scenarios/programs/invalid_opcodes.py new file mode 100644 index 0000000000..cfcc5af33b --- /dev/null +++ b/tests/frontier/scenarios/programs/invalid_opcodes.py @@ -0,0 +1,50 @@ +""" +Define programs that can not be run in static context +""" + +import pytest + +from ethereum_test_tools import Bytecode +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from ..common import ProgramResult + +invalid_opcode_ranges = [ + range(0x0C, 0x10), + range(0x1E, 0x20), + range(0x21, 0x30), + range(0x4B, 0x50), + range(0xA5, 0xF0), + range(0xF6, 0xFA), + range(0xFB, 0xFD), + range(0xFE, 0xFF), +] + + +def make_all_invalid_opcode_calls() -> Bytecode: + """Call special contract to initiate all invalid opcode instruction""" + invalid_opcode_caller = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD + + code = Bytecode( + sum( + [ + Op.MSTORE(64, opcode) + + Op.MSTORE(32, Op.CALL(100000, invalid_opcode_caller, 0, 64, 32, 0, 0)) + + Op.MSTORE(0, Op.ADD(Op.MLOAD(0), Op.MLOAD(32))) + for opcode_range in invalid_opcode_ranges + for opcode in opcode_range + ], + start=Bytecode(), + ) + # If any of invalid instructions works, mstore[0] will be > 1 + + Op.MSTORE(0, Op.ADD(Op.MLOAD(0), 1)) + + Op.RETURN(0, 32) + ) + return code + + +program_invalid = pytest.param( + make_all_invalid_opcode_calls(), + ProgramResult(result=1), + id="program_INVALID", +) diff --git a/tests/frontier/scenarios/programs/static_violation.py b/tests/frontier/scenarios/programs/static_violation.py new file mode 100644 index 0000000000..7460e307c7 --- /dev/null +++ b/tests/frontier/scenarios/programs/static_violation.py @@ -0,0 +1,45 @@ +""" +Define programs that can not be run in static context +""" + +import pytest + +from ethereum_test_forks import Cancun +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from ..common import ProgramResult + +program_sstore_sload = pytest.param( + Op.SSTORE(0, 11) + + Op.MSTORE(0, Op.ADD(Op.MLOAD(0), Op.SLOAD(0))) + + Op.SSTORE(0, 5) + + Op.MSTORE(0, Op.ADD(Op.MLOAD(0), Op.SLOAD(0))) + + Op.RETURN(0, 32), + ProgramResult(result=16, static_support=False), + id="program_SSTORE_SLOAD", +) + +program_tstore_tload = pytest.param( + Op.TSTORE(0, 11) + Op.MSTORE(0, Op.TLOAD(0)) + Op.RETURN(0, 32), + ProgramResult(result=11, static_support=False, from_fork=Cancun), + id="program_TSTORE_TLOAD", +) + +program_logs = pytest.param( + Op.MSTORE(0, 0x1122334455667788991011121314151617181920212223242526272829303132) + + Op.LOG0(0, 1) + + Op.LOG1(1, 1, 0x1000) + + Op.LOG2(2, 1, 0x2000, 0x2001) + + Op.LOG3(3, 1, 0x3000, 0x3001, 0x3002) + + Op.LOG4(4, 1, 0x4000, 0x4001, 0x4002, 0x4003) + + Op.MSTORE(0, 1) + + Op.RETURN(0, 32), + ProgramResult(result=1, static_support=False), + id="program_LOGS", +) + +program_suicide = pytest.param( + Op.MSTORE(0, 1) + Op.SELFDESTRUCT(0) + Op.RETURN(0, 32), + ProgramResult(result=0, static_support=False), + id="program_SUICIDE", +) diff --git a/tests/frontier/scenarios/scenarios/__init__.py b/tests/frontier/scenarios/scenarios/__init__.py new file mode 100644 index 0000000000..20f2893c0e --- /dev/null +++ b/tests/frontier/scenarios/scenarios/__init__.py @@ -0,0 +1,3 @@ +""" +Scenarios common import +""" diff --git a/tests/frontier/scenarios/scenarios/call_combinations.py b/tests/frontier/scenarios/scenarios/call_combinations.py new file mode 100644 index 0000000000..12334eb29d --- /dev/null +++ b/tests/frontier/scenarios/scenarios/call_combinations.py @@ -0,0 +1,306 @@ +""" +Define Scenario that will put a given program in all call contexts +""" + +from dataclasses import dataclass +from typing import List + +from ethereum_test_tools import Address +from ethereum_test_tools.vm.opcode import Opcode +from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_vm import EVMCodeType + +from ..common import Scenario, ScenarioEnvironment, ScenarioGeneratorInput + + +class ScenariosCallCombinations: + """ + Class that would generate scenarios for all call combinations + """ + + @dataclass + class AddressBalance: + """ + Definition of values we use to put in contract balances and call + """ + + root_call_value = 1 + first_call_value = 3 + second_call_value = 5 + + root_contract_balance = 105 + scenario_contract_balance = 107 + sub_contract_balance = 111 + program_selfbalance = 113 + + """The gas we keep before calling an address""" + keep_gas = 100000 + + """Possible calls list to make as a first call""" + first_calls: List[Opcode] = [] + + """Possible calls list to make as a second call""" + second_calls: List[Opcode] = [] + + """Balance map that we put in different accounts""" + balance: AddressBalance + input: ScenarioGeneratorInput + + env: ScenarioEnvironment + + def __init__(self, input: ScenarioGeneratorInput): + """ + Define possible call combinations given the fork + """ + self.first_calls = [ + callcode + for callcode, evm_type in input.fork.call_opcodes() + if evm_type == EVMCodeType.LEGACY + ] + self.second_calls = [ + callcode + for callcode, evm_type in input.fork.call_opcodes() + if evm_type == EVMCodeType.LEGACY + ] + self.second_calls.append(Op.NOOP) + self.input = input + self.balance = self.AddressBalance() + + def generate(self) -> List[Scenario]: + """ + Generate Scenarios for call combinations + We take code that we want to test at input.operation_contract + and put it in the context of call combinations. + + Example: + root_contract -> call -> scenario_contract -> first_call -> sub_contract + sub_contact -> second_call -> code + We assume that code always returns it's result + That we pass as return value in scenario_contract for the post state verification + """ + list: List[Scenario] = [] + + for first_call in self.first_calls: + for second_call in self.second_calls: + if second_call == Op.NOOP: + self._generate_one_call_scenarios(first_call, list) + else: + self._generate_two_call_scenarios(first_call, second_call, list) + return list + + def _generate_one_call_scenarios(self, first_call: Opcode, list: List[Scenario]): + """ + Generate scenario for only one call + root_contract -(CALL)-> scenario_contract -(first_call)-> operation_contract + """ + input = self.input + balance = self.balance + operation_contract = input.pre.deploy_contract( + code=input.operation_code, balance=balance.program_selfbalance + ) + + scenario_contract = input.pre.deploy_contract( + code=Op.MSTORE(32, input.external_address) + + first_call( + gas=Op.SUB(Op.GAS, self.keep_gas), + address=operation_contract, + args_offset=32, + args_size=40, + ret_size=32, + value=balance.first_call_value, + ) + + Op.RETURN(0, 32), + balance=balance.scenario_contract_balance, + ) + + root_contract = input.pre.deploy_contract( + code=Op.CALL( + gas=Op.SUB(Op.GAS, self.keep_gas), + address=scenario_contract, + ret_size=32, + ) + + Op.RETURN(0, 32), + balance=balance.root_contract_balance, + ) + + list.append( + Scenario( + name=f"scenario_{first_call}", + code=root_contract, + env=ScenarioEnvironment( + # Define address on which behalf program is executed + code_address=( + scenario_contract + if first_call == Op.CALLCODE or first_call == Op.DELEGATECALL + else operation_contract + ), + # Define code_caller for Op.CALLER + code_caller=( + root_contract if first_call == Op.DELEGATECALL else scenario_contract + ), + # Define balance for Op.BALANCE + selfbalance=( + balance.scenario_contract_balance + if first_call in [Op.DELEGATECALL, Op.CALLCODE] + else ( + balance.program_selfbalance + if first_call == Op.STATICCALL + else balance.first_call_value + balance.program_selfbalance + ) + ), + ext_balance=input.external_balance, + call_value=( + 0 + if first_call in [Op.STATICCALL, Op.DELEGATECALL] + else balance.first_call_value + ), + call_dataload_0=int(input.external_address.hex(), 16), + call_datasize=40, + has_static=True if first_call == Op.STATICCALL else False, + ), + ) + ) + + def _generate_two_call_scenarios( + self, first_call: Opcode, second_call: Opcode, list: List[Scenario] + ): + """ + Generate scenario for two types of calls combination + root_contract -(CALL)-> scenario_contract -(first_call)-> sub_contract + sub_contract -(second_call) -> operation_contract + """ + + def _compute_code_caller() -> Address: + """ + Calculate who is the code caller in program_contract's code in given sequence + root -CALL-> scenario_contract -(first_call)-> sub_contract -(second_call)-> program + """ + code_caller: Address = root_contract + if first_call == Op.DELEGATECALL: + code_caller = scenario_contract + if second_call == Op.DELEGATECALL: + code_caller = root_contract + else: + if second_call == Op.DELEGATECALL: + code_caller = scenario_contract + else: + code_caller = sub_contract + if first_call == Op.CALLCODE: + code_caller = scenario_contract + return code_caller + + def _compute_selfbalance() -> int: + """ + Calculate the result of Op.SELFBALANCE in program scope in given sequence + root -CALL-> scenario_contract -(first_call)-> sub_contract -(second_call)-> program + """ + selfbalance: int = 0 + if second_call in [Op.CALL]: + selfbalance = second_call_value + balance.program_selfbalance + return selfbalance + if second_call in [Op.STATICCALL]: + selfbalance = balance.program_selfbalance + return selfbalance + if first_call == Op.STATICCALL and second_call in [Op.DELEGATECALL, Op.CALLCODE]: + selfbalance = balance.sub_contract_balance + if first_call in [Op.CALLCODE, Op.DELEGATECALL] and second_call in [ + Op.DELEGATECALL, + Op.CALLCODE, + ]: + selfbalance = balance.scenario_contract_balance + balance.root_call_value + if first_call == Op.CALL and second_call in [Op.DELEGATECALL, Op.CALLCODE]: + selfbalance = balance.sub_contract_balance + balance.first_call_value + if first_call == Op.STATICCALL and second_call == Op.STATICCALL: + selfbalance = balance.program_selfbalance + return selfbalance + + def _compute_callvalue() -> int: + """ + Calculate the expected callvalue in program scope given sequence: + root -CALL-> scenario_contract -(first_call)-> sub_contract -(second_call)-> program + """ + if second_call == Op.STATICCALL: + return 0 + if second_call == Op.DELEGATECALL: + if first_call == Op.STATICCALL: + return 0 + else: + if first_call == Op.DELEGATECALL: + return balance.root_call_value + else: + return balance.first_call_value + else: + return second_call_value + + input = self.input + balance = self.balance + second_call_value = balance.second_call_value if first_call != Op.STATICCALL else 0 + + operation_contract = input.pre.deploy_contract( + code=input.operation_code, balance=balance.program_selfbalance + ) + sub_contract = input.pre.deploy_contract( + code=Op.MSTORE(32, input.external_address) + + second_call( + gas=Op.SUB(Op.GAS, self.keep_gas), + address=operation_contract, + args_size=40, + args_offset=32, + ret_size=32, + value=second_call_value, + ) + + Op.RETURN(0, 32), + balance=balance.sub_contract_balance, + ) + scenario_contract = input.pre.deploy_contract( + code=first_call( + gas=Op.SUB(Op.GAS, self.keep_gas), + address=sub_contract, + ret_size=32, + value=balance.first_call_value, + ) + + Op.RETURN(0, 32), + balance=balance.scenario_contract_balance, + ) + + root_contract = input.pre.deploy_contract( + balance=balance.root_contract_balance, + code=Op.CALL( + gas=Op.SUB(Op.GAS, self.keep_gas), + address=scenario_contract, + ret_size=32, + value=balance.root_call_value, + ) + + Op.RETURN(0, 32), + ) + + list.append( + Scenario( + name=f"scenario_{first_call}_{second_call}", + code=root_contract, + env=ScenarioEnvironment( + # Define address on which behalf program is executed + code_address=( + operation_contract + if second_call not in [Op.CALLCODE, Op.DELEGATECALL] + else ( + sub_contract + if first_call not in [Op.CALLCODE, Op.DELEGATECALL] + else scenario_contract + ) + ), + # Define code_caller for Op.CALLER + code_caller=_compute_code_caller(), + selfbalance=_compute_selfbalance(), + ext_balance=input.external_balance, + call_value=_compute_callvalue(), + call_dataload_0=int(input.external_address.hex(), 16), + call_datasize=40, + has_static=( + True + if first_call == Op.STATICCALL or second_call == Op.STATICCALL + else False + ), + ), + ) + ) diff --git a/tests/frontier/scenarios/scenarios/create_combinations.py b/tests/frontier/scenarios/scenarios/create_combinations.py new file mode 100644 index 0000000000..6bc51bf79e --- /dev/null +++ b/tests/frontier/scenarios/scenarios/create_combinations.py @@ -0,0 +1,178 @@ +""" +Define Scenario that will put a given program in create contexts +""" + +from dataclasses import dataclass +from typing import List + +from ethereum_test_tools import Bytecode +from ethereum_test_tools.vm.opcode import Macros as Om +from ethereum_test_tools.vm.opcode import Opcode +from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_types import compute_create_address +from ethereum_test_vm import EVMCodeType + +from ..common import Scenario, ScenarioEnvironment, ScenarioGeneratorInput + + +@dataclass +class AddressBalance: + """ + Definition of values we use to put in contract balances and call + """ + + root_call_value = 1 + create_value = 3 + call_value = 5 + root_contract_balance = 100 + scenario_contract_balance = 200 + + +def scenarios_create_combinations(input: ScenarioGeneratorInput) -> List[Scenario]: + """Generate Scenarios for create combinations""" + + def _compute_selfbalance() -> int: + """ + Compute selfbalance opcode for root -> call -> scenario -> create | [call*] -> program + """ + if call in [Op.DELEGATECALL, Op.CALLCODE]: + return ( + balance.scenario_contract_balance + balance.root_call_value - balance.create_value + ) + if call == Op.CALL: + return balance.create_value + balance.call_value + return balance.create_value + + list: List[Scenario] = [] + keep_gas = 100000 + create_types: List[Opcode] = [ + create_code + for create_code, evm_type in input.fork.create_opcodes() + if evm_type == EVMCodeType.LEGACY + ] + env: ScenarioEnvironment + balance: AddressBalance = AddressBalance() + + # run code in create constructor + for create in create_types: + salt = [0] if create == Op.CREATE2 else [] + operation_contract = input.pre.deploy_contract(code=input.operation_code) + + # the code result in init code will be actually code of a deployed contract + scenario_contract = input.pre.deploy_contract( + balance=3, + code=Op.EXTCODECOPY(operation_contract, 0, 0, Op.EXTCODESIZE(operation_contract)) + + Op.MSTORE(0, create(3, 0, Op.EXTCODESIZE(operation_contract), *salt)) + + Op.EXTCODECOPY(Op.MLOAD(0), 0, 0, 32) + + Op.RETURN(0, 32), + ) + + created_address = compute_create_address( + address=scenario_contract, + nonce=1, + initcode=input.operation_code, + opcode=Op.CREATE if create == Op.CREATE else Op.CREATE2, + ) + + env = ScenarioEnvironment( + # Define address on which behalf program is executed + code_address=created_address, + code_caller=scenario_contract, + selfbalance=3, + ext_balance=input.external_balance, + call_value=3, + call_dataload_0=0, + call_datasize=0, + ) + list.append( + Scenario( + name=f"scenario_{create}_constructor", + code=scenario_contract, + env=env, + ) + ) + + # create a contract with test code and call it + deploy_code = Bytecode( + Op.EXTCODECOPY(operation_contract, 0, 0, Op.EXTCODESIZE(operation_contract)) + + Op.RETURN(0, Op.EXTCODESIZE(operation_contract)) + ) + deploy_code_size: int = int(len(deploy_code.hex()) / 2) + call_types: List[Opcode] = [ + callcode + for callcode, evm_type in input.fork.call_opcodes() + if evm_type == EVMCodeType.LEGACY + ] + + for create in create_types: + for call in call_types: + salt = [0] if create == Op.CREATE2 else [] + + scenario_contract = input.pre.deploy_contract( + balance=balance.scenario_contract_balance, + code=Om.MSTORE(deploy_code, 0) + + Op.MSTORE(32, create(balance.create_value, 0, deploy_code_size, *salt)) + + Op.MSTORE(0, 0) + + Op.MSTORE(64, input.external_address) + + call( + gas=Op.SUB(Op.GAS, keep_gas), + address=Op.MLOAD(32), + args_offset=64, + args_size=40, + ret_offset=0, + ret_size=32, + value=balance.call_value, + ) + + Op.RETURN(0, 32), + ) + + root_contract = input.pre.deploy_contract( + balance=balance.root_contract_balance, + code=Op.CALL( + gas=Op.SUB(Op.GAS, keep_gas), + address=scenario_contract, + ret_size=32, + value=balance.root_call_value, + ) + + Op.RETURN(0, 32), + ) + + created_address = compute_create_address( + address=scenario_contract, + nonce=1, + initcode=deploy_code, + opcode=Op.CREATE if create == Op.CREATE else Op.CREATE2, + ) + + env = ScenarioEnvironment( + # Define address on which behalf program is executed + code_address=( + scenario_contract + if call in [Op.CALLCODE, Op.DELEGATECALL] + else created_address + ), + code_caller=root_contract if call == Op.DELEGATECALL else scenario_contract, + selfbalance=_compute_selfbalance(), + ext_balance=input.external_balance, + call_value=( + 0 + if call in [Op.STATICCALL] + else ( + balance.root_call_value + if call in [Op.DELEGATECALL] + else balance.call_value + ) + ), + call_dataload_0=int(input.external_address.hex(), 16), + call_datasize=40, + has_static=True if call == Op.STATICCALL else False, + ) + list.append( + Scenario( + name=f"scenario_{create}_then_{call}", + code=root_contract, + env=env, + ) + ) + + return list diff --git a/tests/frontier/scenarios/scenarios/revert_combinations.py b/tests/frontier/scenarios/scenarios/revert_combinations.py new file mode 100644 index 0000000000..dff9bcef02 --- /dev/null +++ b/tests/frontier/scenarios/scenarios/revert_combinations.py @@ -0,0 +1,48 @@ +""" +Define Scenario that will run a given program and then revert +""" + +from typing import List + +from ethereum_test_tools.vm.opcode import Macro +from ethereum_test_tools.vm.opcode import Macros as Om +from ethereum_test_tools.vm.opcode import Opcode +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from ..common import Scenario, ScenarioEnvironment, ScenarioGeneratorInput + + +def scenarios_revert_combinations(input: ScenarioGeneratorInput) -> List[Scenario]: + """Generate Scenarios for revert combinations""" + list: List[Scenario] = [] + keep_gas = 100000 + # TODO stack underflow cause + revert_types: List[Opcode | Macro] = [Op.STOP, Om.OOG] + if Op.REVERT in input.fork.valid_opcodes(): + revert_types.append(Op.REVERT) + for revert in revert_types: + operation_contract = input.pre.deploy_contract(code=input.operation_code) + scenario_contract = input.pre.deploy_contract( + code=Op.CALLCODE(gas=Op.SUB(Op.GAS, keep_gas), address=operation_contract, ret_size=32) + + revert(0, 32, unchecked=True) + + Op.RETURN(0, 32) + ) + env: ScenarioEnvironment = ScenarioEnvironment( + code_address=scenario_contract, + code_caller=scenario_contract, + selfbalance=0, + ext_balance=input.external_balance, + call_value=0, + call_dataload_0=0, + call_datasize=0, + ) + list.append( + Scenario( + name=f"scenario_revert_by_{revert}", + code=scenario_contract, + env=env, + halts=False if revert == Op.REVERT else True, + ) + ) + + return list diff --git a/tests/frontier/scenarios/test_scenarios.py b/tests/frontier/scenarios/test_scenarios.py new file mode 100644 index 0000000000..7b8b417b05 --- /dev/null +++ b/tests/frontier/scenarios/test_scenarios.py @@ -0,0 +1,269 @@ +""" +Call every possible opcode and test that the subcall is successful +if the opcode is supported by the fork and fails otherwise. +""" + +from typing import List + +import pytest + +from ethereum_test_forks import Fork +from ethereum_test_tools import ( + Account, + Address, + Alloc, + Bytecode, + Environment, + Hash, + StateTestFiller, + Storage, + Transaction, +) +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from .common import ( + ExecutionEnvironment, + ProgramResult, + Scenario, + ScenarioDebug, + ScenarioGeneratorInput, + replace_special_calls_in_operation, + translate_result, +) +from .programs.all_frontier_opcodes import program_all_frontier_opcodes +from .programs.context_calls import ( + program_address, + program_balance, + program_basefee, + program_blobbasefee, + program_blobhash, + program_blockhash, + program_calldatacopy, + program_calldataload, + program_calldatasize, + program_caller, + program_callvalue, + program_chainid, + program_codecopy_codesize, + program_coinbase, + program_difficulty_randao, + program_ext_codecopy_codesize, + program_extcodehash, + program_gaslimit, + program_gasprice, + program_mcopy, + program_number, + program_origin, + program_push0, + program_returndatacopy, + program_returndatasize, + program_selfbalance, + program_timestamp, + program_tload, +) +from .programs.invalid_opcodes import program_invalid +from .programs.static_violation import ( + program_logs, + program_sstore_sload, + program_suicide, + program_tstore_tload, +) +from .scenarios.call_combinations import ScenariosCallCombinations +from .scenarios.create_combinations import scenarios_create_combinations +from .scenarios.revert_combinations import scenarios_revert_combinations + +REFERENCE_SPEC_GIT_PATH = "N/A" + +REFERENCE_SPEC_VERSION = "N/A" + + +@pytest.fixture +def scenarios(fork: Fork, pre: Alloc, operation: Bytecode, debug: ScenarioDebug) -> List[Scenario]: + """ + This is the main parametrization vector + Define list of contracts that execute scenarios for a given operation + """ + list: List[Scenario] = [] + + """select only debug program if set""" + if debug.test_param is not None: + debug_program = debug.test_param.values[0] + if hasattr(debug_program, "hex") and operation.hex() != debug_program.hex(): + return list + + """Deploy external address to test ext opcodes""" + external_balance = 123 + external_address = pre.deploy_contract(code=Op.ADD(1, 1), balance=external_balance) + + operation = replace_special_calls_in_operation(pre, operation, external_address) + + input: ScenarioGeneratorInput = ScenarioGeneratorInput( + fork=fork, + pre=pre, + operation_code=operation, + external_address=external_address, + external_balance=external_balance, + ) + + call_combinations = ScenariosCallCombinations(input).generate() + for combination in call_combinations: + if not debug.scenario_name or combination.name == debug.scenario_name: + list.append(combination) + + call_combinations = scenarios_create_combinations(input) + for combination in call_combinations: + if not debug.scenario_name or combination.name == debug.scenario_name: + list.append(combination) + + revert_combinations = scenarios_revert_combinations(input) + for combination in revert_combinations: + if not debug.scenario_name or combination.name == debug.scenario_name: + list.append(combination) + + """ + // 21. 0x00FD Run the code, call a contract that reverts, then run again + // 22. 0x00FE Run the code, call a contract that goes out of gas, then run again + // 23. 0x00FF Run the code, call a contract that self-destructs, then run again + // 34. 0x60BACCFA57 Call recurse to the limit + """ + + return list + + +@pytest.mark.valid_from("Frontier") +@pytest.mark.parametrize( + # select program to debug (program, "scenario_name") + # program=None select all programs + # scenario_name="" select all scenarios + "debug", + [ScenarioDebug(None, scenario_name="")], + ids=["debug"], +) +@pytest.mark.parametrize( + "operation, expected_result", + [ + # invalid opcodes + program_invalid, + # static violation programs + program_sstore_sload, + program_tstore_tload, + program_logs, + # 00 - 20 + program_all_frontier_opcodes, + # 30 + program_address, + program_balance, + program_origin, + program_caller, + program_callvalue, + program_calldataload, + program_calldatasize, + program_calldatacopy, + program_codecopy_codesize, + program_gasprice, + program_ext_codecopy_codesize, + program_returndatasize, + program_returndatacopy, + program_extcodehash, + # 40 + program_blockhash, # fails in state mode in py-t8n + program_coinbase, + program_timestamp, + program_number, + program_difficulty_randao, + program_gaslimit, + program_chainid, + program_selfbalance, + program_basefee, + program_blobhash, + program_blobbasefee, + program_tload, + program_mcopy, + program_push0, + program_suicide, + ], +) +def test_scenarios( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + expected_result: ProgramResult, + scenarios, +): + """ + Test given operation in different scenarios + Verify that it's return value equal to expected result on every scenario, + that is valid for the given fork + + Note: Don't use pytest parametrize for scenario production, because scenarios will be complex + Generate one test file for [each operation] * [each scenario] to save space + As well as operations will be complex too + """ + # Skip disabled scenarios in debug mode + if len(scenarios) == 0: + return + + tx_env = Environment() + tx_env.set_fork_requirements(fork) + post = Storage() + result_slot = post.store_next(1, hint="runner_result") + + tx_origin: Address = pre.fund_eoa() + tx_gasprice: int = 10 + exec_env = ExecutionEnvironment( + fork=fork, + origin=tx_origin, + gasprice=tx_gasprice, + timestamp=tx_env.timestamp, + number=tx_env.number, + gaslimit=tx_env.gas_limit, + coinbase=tx_env.fee_recipient, + # how to pre calculate this values: + blob_basefee=0, # because we are in frontier test folder + blobhash_0=Hash(0), # because we are in frontier test folder + blockhash_0=Hash(0), + difficulty_randao=0 if tx_env.difficulty is None else int(tx_env.difficulty.hex(), 16), + basefee=0 if tx_env.base_fee_per_gas is None else int(tx_env.base_fee_per_gas.hex(), 16), + ) + + def make_result(scenario: Scenario) -> int: + """Make Scenario post result""" + if scenario.halts: + return post.store_next(0, hint=scenario.name) + else: + return post.store_next( + translate_result(expected_result, scenario.env, exec_env), hint=scenario.name + ) + + runner_contract = pre.deploy_contract( + code=sum( + Op.MSTORE(0, 0) + + Op.CALL(10000000, scenario.code, 0, 0, 0, 0, 32) + + Op.SSTORE(make_result(scenario), Op.MLOAD(0)) + for scenario in scenarios + ) + + Op.SSTORE(result_slot, 1), + storage={ + result_slot: 0xFFFF, + }, + ) + + tx = Transaction( + sender=tx_origin, + gas_limit=500_000_000, + gas_price=tx_gasprice, + to=runner_contract, + data=b"0x11223344", + value=0, + protected=False, + ) + state_test( + env=tx_env, + pre=pre, + post={ + runner_contract: Account( + storage=post, + ) + }, + tx=tx, + ) diff --git a/whitelist.txt b/whitelist.txt index 7eaa4fa2f2..7988e11058 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -750,10 +750,16 @@ tT istanbul berlin +0A +0B 0C 0D 0E 0F +1A +1B +1C +1D 1E 1F 2A @@ -762,17 +768,64 @@ berlin 2D 2E 2F +3A +3B +3C +3D +3E +3F +4A 4B 4C 4D 4E 4F +5A +5B +5C +5D +5E +5F +6A +6B +6C +6D +6E +6F +7A +7B +7C +7D +7E +7F +8A +8B +8C +8D +8E +8F +9A +9B +9C +9D +9E +9F +A0 +A1 +A2 +A3 +A4 A5 A6 A7 A8 A9 +AA +AB +AC +AD AE +AF B0 B1 B2 @@ -783,7 +836,12 @@ B6 B7 B8 B9 +BA +BB +BC BD +BE +BF C0 C1 C2 @@ -794,15 +852,57 @@ C6 C7 C8 C9 +CA +CB +CC +CD +CE +CF +D0 +D1 +D2 +D3 D4 D5 D6 D7 D8 D9 +DA +DB +DC +DE DF +DF +E0 +E1 +E2 +E3 +E4 +E5 +E6 +E7 +E8 E9 +EA EB +EC +ED +EE EF +F0 +F1 +F2 +F3 +F4 +F5 F6 +F7 +F8 +F9 +FA +FB FC +FD +FE +FF \ No newline at end of file