From 1857e0562df7cc2867f037ca33514566a8d87101 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 15 May 2025 17:58:16 +0000 Subject: [PATCH 1/2] feat(vm): Implement `MemoryVariable` --- docs/CHANGELOG.md | 1 + src/ethereum_test_tools/__init__.py | 2 + src/ethereum_test_vm/__init__.py | 3 +- src/ethereum_test_vm/helpers.py | 75 +++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b5192736deb..d53b051c6ba 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -29,6 +29,7 @@ The output behavior of `fill` has changed ([#1608](https://github.com/ethereum/e - ✨ Added the [EIP checklist template](https://eest.ethereum.org/main/writing_tests/checklist_templates/eip_testing_checklist_template/) that serves as a reference to achieve better coverage when implementing tests for new EIPs ([#1327](https://github.com/ethereum/execution-spec-tests/pull/1327)). - ✨ Added [Post-Mortems of Missed Test Scenarios](https://eest.ethereum.org/main/writing_tests/post_mortems/) to the documentation that serves as a reference list of all cases that were missed during the test implementation phase of a new EIP, and includes the steps taken in order to prevent similar test cases to be missed in the future ([#1327](https://github.com/ethereum/execution-spec-tests/pull/1327)). +- ✨ Added the `MemoryVariable` EVM abstraction to generate more readable bytecode when there's heavy use of variables that are stored in memory ([#1609](https://github.com/ethereum/execution-spec-tests/pull/1609)). ### 🧪 Test Cases diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 62965743297..f7f336773f4 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -65,6 +65,7 @@ EVMCodeType, Macro, Macros, + MemoryVariable, Opcode, OpcodeCallArg, Opcodes, @@ -127,6 +128,7 @@ "Initcode", "Macro", "Macros", + "MemoryVariable", "NetworkWrappedTransaction", "Opcode", "OpcodeCallArg", diff --git a/src/ethereum_test_vm/__init__.py b/src/ethereum_test_vm/__init__.py index 34972a73a42..d55e5c0185e 100644 --- a/src/ethereum_test_vm/__init__.py +++ b/src/ethereum_test_vm/__init__.py @@ -2,7 +2,7 @@ from .bytecode import Bytecode from .evm_types import EVMCodeType -from .helpers import call_return_code +from .helpers import MemoryVariable, call_return_code from .opcode import Macro, Macros, Opcode, OpcodeCallArg, Opcodes, UndefinedOpcodes __all__ = ( @@ -10,6 +10,7 @@ "EVMCodeType", "Macro", "Macros", + "MemoryVariable", "Opcode", "OpcodeCallArg", "Opcodes", diff --git a/src/ethereum_test_vm/helpers.py b/src/ethereum_test_vm/helpers.py index 7460975f632..0589df34328 100644 --- a/src/ethereum_test_vm/helpers.py +++ b/src/ethereum_test_vm/helpers.py @@ -1,8 +1,83 @@ """Helper functions for the EVM.""" +from .bytecode import Bytecode from .opcode import Opcodes as Op +class MemoryVariable(Bytecode): + """ + Variable abstraction to help keep track values that are stored in memory. + + To use, simply declare a variable with an unique offset that is not used by any other variable. + + The variable then can be used in-place to read the value from memory: + + ```python + v = MemoryVariable(128) + + bytecode = Op.ADD(v, Op.CALLDATASIZE()) + ``` + + The previous example is equivalent to: + + ```python + bytecode = Op.ADD(Op.MLOAD(offset=128), Op.CALLDATASIZE()) + ``` + + The variable also contains methods to add and subtract values from the memory offset. + + ```python + v = MemoryVariable(128) + + bytecode = ( + v.store(0xff) + + v.add(1) + + v.return_value() + ) + ``` + + The previous example is equivalent to: + + ```python + bytecode = ( + Op.MSTORE(offset=128, value=0xff) + + Op.MSTORE(offset=128, value=Op.ADD(Op.MLOAD(offset=128), 1)) + + Op.RETURN(offset=128, size=32) + ) + ``` + + """ + + offset: int + + def __new__(cls, offset: int): + """ + Initialize EVM memory variable. + + When used with normal bytecode, this class simply returns the MLOAD with the provided + offset. + """ + instance = super().__new__(cls, Op.MLOAD(offset=offset)) + instance.offset = offset + return instance + + def store(self, value: int | Bytecode): + """Store value in memory.""" + return Op.MSTORE(offset=self.offset, value=value) + + def add(self, value: int | Bytecode): + """Add value to memory.""" + return Op.MSTORE(offset=self.offset, value=Op.ADD(Op.MLOAD(offset=self.offset), value)) + + def sub(self, value: int | Bytecode): + """Subtract value from memory.""" + return Op.MSTORE(offset=self.offset, value=Op.SUB(Op.MLOAD(offset=self.offset), value)) + + def return_value(self): + """Return value from memory.""" + return Op.RETURN(offset=self.offset, size=32) + + def call_return_code(opcode: Op, success: bool, *, revert: bool = False) -> int: """Return return code for a CALL operation.""" if opcode in [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL]: From f95558d779282d182e239002b025452ef781e89a Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 15 May 2025 18:06:53 +0000 Subject: [PATCH 2/2] refactor(tests): Use MemoryVariable in tests/frontier/scenarios/common.py --- tests/frontier/scenarios/common.py | 66 +++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/tests/frontier/scenarios/common.py b/tests/frontier/scenarios/common.py index b56d941ecb1..cb637ca54fb 100644 --- a/tests/frontier/scenarios/common.py +++ b/tests/frontier/scenarios/common.py @@ -5,8 +5,8 @@ from enum import Enum from ethereum_test_forks import Fork, Frontier -from ethereum_test_tools import Address, Alloc, Bytecode, Conditional -from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_tools import Address, Alloc, Bytecode, Conditional, MemoryVariable +from ethereum_test_tools import Opcodes as Op class ScenarioExpectOpcode(Enum): @@ -190,20 +190,40 @@ def make_gas_hash_contract(pre: Alloc) -> Address: So that if we can't check exact value in expect section, we at least could spend unique gas amount. """ + # EVM memory variables + variable_byte_offset = MemoryVariable(0) + variable_current_byte = MemoryVariable(32) + + # Code for memory initialization + initialize_code = variable_byte_offset.store(0) + calldata_copy = Op.JUMPDEST + Op.CALLDATACOPY( + dest_offset=variable_current_byte.offset + 32 - 1, + offset=variable_byte_offset, + size=1, + ) + + # Code offsets + offset_calldata_copy = len(initialize_code) + offset_conditional = offset_calldata_copy + len(calldata_copy) + + # Deploy contract gas_hash_address = pre.deploy_contract( - code=Op.MSTORE(0, 0) - + Op.JUMPDEST - + Op.CALLDATACOPY(63, Op.MLOAD(0), 1) - + Op.JUMPDEST + code=initialize_code + + calldata_copy # offset_calldata_copy + + Op.JUMPDEST # offset_conditional + Conditional( - condition=Op.ISZERO(Op.MLOAD(32)), - 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), + condition=Op.ISZERO(variable_current_byte), + if_true=( + # Increase the calldata byte offset, and if it's greater than the calldata size, + # return, otherwise jump to the calldata copy code and read the next byte. + variable_byte_offset.add(1) + + Conditional( + condition=Op.GT(variable_byte_offset, Op.CALLDATASIZE()), + if_true=Op.RETURN(offset=0, size=0), + if_false=Op.JUMP(offset_calldata_copy), + ) ), - if_false=Op.MSTORE(32, Op.SUB(Op.MLOAD(32), 1)) + Op.JUMP(14), + if_false=(variable_current_byte.sub(1) + Op.JUMP(offset_conditional)), ) ) return gas_hash_address @@ -234,19 +254,25 @@ def make_invalid_opcode_contract(pre: Alloc, fork: Fork) -> Address: if op not in valid_opcode_values: invalid_opcodes.append(op) + variable_results_sum = MemoryVariable(0) + variable_opcode = MemoryVariable(32) + code = Bytecode( sum( - Op.MSTORE(64, opcode) - + Op.MSTORE( - 32, - Op.CALL(gas=50000, address=invalid_opcode_caller, args_offset=64, args_size=32), + variable_opcode.store(opcode) + + variable_results_sum.add( + Op.CALL( + gas=50000, + address=invalid_opcode_caller, + args_offset=variable_opcode.offset, + args_size=32, + ), ) - + Op.MSTORE(0, Op.ADD(Op.MLOAD(0), Op.MLOAD(32))) for opcode in invalid_opcodes ) # If any of invalid instructions works, mstore[0] will be > 1 - + Op.MSTORE(0, Op.ADD(Op.MLOAD(0), 1)) - + Op.RETURN(0, 32) + + variable_results_sum.add(1) + + variable_results_sum.return_value() ) return pre.deploy_contract(code=code)