Skip to content

Commit 9fc77e2

Browse files
marioevzspencer-tb
andauthored
feat(vm): evm memory variable abstraction (#1609)
* feat(vm): Implement `MemoryVariable` * refactor(tests): Use MemoryVariable in tests/frontier/scenarios/common.py * Rename "store" to "set" * Fixup * refactor * chore: ruff 79 line lenght. --------- Co-authored-by: spencer-tb <[email protected]>
1 parent cc753ac commit 9fc77e2

File tree

5 files changed

+135
-20
lines changed

5 files changed

+135
-20
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Test fixtures for use by clients are available for each release on the [Github r
3131
- 🐞 Use `engine_newPayloadV5` for `>=Amsterdam` forks in `consume engine` ([#2170](https://github.com/ethereum/execution-spec-tests/pull/2170)).
3232
- 🔀 Refactor EIP-7928 (BAL) absence checks into a friendlier class-based DevEx ([#2175](https://github.com/ethereum/execution-spec-tests/pull/2175)).
3333
- 🐞 Tighten up validation for empty lists on Block-Level Access List tests ([#2118](https://github.com/ethereum/execution-spec-tests/pull/2118)).
34+
- ✨ 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)).
3435

3536
### 🧪 Test Cases
3637

src/ethereum_test_tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
EVMCodeType,
8383
Macro,
8484
Macros,
85+
MemoryVariable,
8586
Opcode,
8687
OpcodeCallArg,
8788
Opcodes,
@@ -157,6 +158,7 @@
157158
"JumpLoopGenerator",
158159
"Macro",
159160
"Macros",
161+
"MemoryVariable",
160162
"NetworkWrappedTransaction",
161163
"Opcode",
162164
"OpcodeCallArg",

src/ethereum_test_vm/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
from .bytecode import Bytecode
44
from .evm_types import EVMCodeType
5-
from .helpers import call_return_code
5+
from .helpers import MemoryVariable, call_return_code
66
from .opcodes import Macro, Macros, Opcode, OpcodeCallArg, Opcodes, UndefinedOpcodes
77

88
__all__ = (
99
"Bytecode",
1010
"EVMCodeType",
1111
"Macro",
1212
"Macros",
13+
"MemoryVariable",
1314
"Opcode",
1415
"OpcodeCallArg",
1516
"Opcodes",

src/ethereum_test_vm/helpers.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,92 @@
11
"""Helper functions for the EVM."""
22

3+
from .bytecode import Bytecode
34
from .opcodes import Opcodes as Op
45

56

7+
class MemoryVariable(Bytecode):
8+
"""
9+
Variable abstraction to help keep track values that are stored in memory.
10+
11+
To use, simply declare a variable with an unique offset that is not used
12+
by any other variable.
13+
14+
The variable then can be used in-place to read the value from memory:
15+
16+
```python
17+
v = MemoryVariable(128)
18+
19+
bytecode = Op.ADD(v, Op.CALLDATASIZE())
20+
```
21+
22+
The previous example is equivalent to:
23+
24+
```python
25+
bytecode = Op.ADD(Op.MLOAD(offset=128), Op.CALLDATASIZE())
26+
```
27+
28+
The variable also contains methods to add and subtract values from the
29+
memory offset.
30+
31+
```python
32+
v = MemoryVariable(128)
33+
34+
bytecode = (
35+
v.set(0xff)
36+
+ v.add(1)
37+
+ v.return_value()
38+
)
39+
```
40+
41+
The previous example is equivalent to:
42+
43+
```python
44+
bytecode = (
45+
Op.MSTORE(offset=128, value=0xff)
46+
+ Op.MSTORE(offset=128, value=Op.ADD(Op.MLOAD(offset=128), 1))
47+
+ Op.RETURN(offset=128, size=32)
48+
)
49+
```
50+
51+
"""
52+
53+
offset: int
54+
55+
def __new__(cls, offset: int):
56+
"""
57+
Instantiate a new EVM memory variable.
58+
59+
When used with normal bytecode, this class simply returns the MLOAD
60+
with the provided offset.
61+
"""
62+
instance = super().__new__(cls, Op.MLOAD(offset=offset))
63+
instance.offset = offset
64+
return instance
65+
66+
def set(self, value: int | Bytecode) -> Bytecode:
67+
"""Set the given value at the memory location of this variable."""
68+
return Op.MSTORE(offset=self.offset, value=value)
69+
70+
def add(self, value: int | Bytecode) -> Bytecode:
71+
"""In-place add the given value to the one currently in memory."""
72+
return Op.MSTORE(offset=self.offset, value=Op.ADD(Op.MLOAD(offset=self.offset), value))
73+
74+
def sub(self, value: int | Bytecode) -> Bytecode:
75+
"""
76+
In-place subtract the given value from the one currently
77+
in memory.
78+
"""
79+
return Op.MSTORE(offset=self.offset, value=Op.SUB(Op.MLOAD(offset=self.offset), value))
80+
81+
def store_value(self, key: int | Bytecode) -> Bytecode:
82+
"""Op.SSTORE the value that is currently in memory."""
83+
return Op.SSTORE(key, Op.MLOAD(offset=self.offset))
84+
85+
def return_value(self) -> Bytecode:
86+
"""Op.RETURN the value that is currently in memory."""
87+
return Op.RETURN(offset=self.offset, size=32)
88+
89+
690
def call_return_code(opcode: Op, success: bool, *, revert: bool = False) -> int:
791
"""Return return code for a CALL operation."""
892
if opcode in [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL]:

tests/frontier/scenarios/common.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from enum import Enum
66

77
from ethereum_test_forks import Fork, Frontier
8-
from ethereum_test_tools import Address, Alloc, Bytecode, Conditional
8+
from ethereum_test_tools import Address, Alloc, Bytecode, Conditional, MemoryVariable
99
from ethereum_test_vm import Opcodes as Op
1010

1111

@@ -196,20 +196,41 @@ def make_gas_hash_contract(pre: Alloc) -> Address:
196196
So that if we can't check exact value in expect section, we at least
197197
could spend unique gas amount.
198198
"""
199+
# EVM memory variables
200+
byte_offset = MemoryVariable(0)
201+
current_byte = MemoryVariable(32)
202+
203+
# Code for memory initialization
204+
initialize_code = byte_offset.set(0)
205+
calldata_copy = Op.JUMPDEST + Op.CALLDATACOPY(
206+
dest_offset=current_byte.offset + 32 - 1,
207+
offset=byte_offset,
208+
size=1,
209+
)
210+
211+
# Code offsets
212+
offset_calldata_copy = len(initialize_code)
213+
offset_conditional = offset_calldata_copy + len(calldata_copy)
214+
215+
# Deploy contract
199216
gas_hash_address = pre.deploy_contract(
200-
code=Op.MSTORE(0, 0)
201-
+ Op.JUMPDEST
202-
+ Op.CALLDATACOPY(63, Op.MLOAD(0), 1)
203-
+ Op.JUMPDEST
217+
code=initialize_code
218+
+ calldata_copy # offset_calldata_copy
219+
+ Op.JUMPDEST # offset_conditional
204220
+ Conditional(
205-
condition=Op.ISZERO(Op.MLOAD(32)),
206-
if_true=Op.MSTORE(0, Op.ADD(1, Op.MLOAD(0)))
207-
+ Conditional(
208-
condition=Op.GT(Op.MLOAD(0), 32),
209-
if_true=Op.RETURN(0, 0),
210-
if_false=Op.JUMP(5),
221+
condition=Op.ISZERO(current_byte),
222+
if_true=(
223+
# Increase the calldata byte offset, and if it's greater than
224+
# the calldata size, return, otherwise jump to the calldata
225+
# copy code and read the next byte.
226+
byte_offset.add(1)
227+
+ Conditional(
228+
condition=Op.GT(byte_offset, Op.CALLDATASIZE()),
229+
if_true=Op.RETURN(offset=0, size=0),
230+
if_false=Op.JUMP(offset_calldata_copy),
231+
)
211232
),
212-
if_false=Op.MSTORE(32, Op.SUB(Op.MLOAD(32), 1)) + Op.JUMP(14),
233+
if_false=(current_byte.sub(1) + Op.JUMP(offset_conditional)),
213234
)
214235
)
215236
return gas_hash_address
@@ -241,19 +262,25 @@ def make_invalid_opcode_contract(pre: Alloc, fork: Fork) -> Address:
241262
if op not in valid_opcode_values:
242263
invalid_opcodes.append(op)
243264

265+
results_sum = MemoryVariable(0)
266+
current_opcode = MemoryVariable(32)
267+
244268
code = Bytecode(
245269
sum(
246-
Op.MSTORE(64, opcode)
247-
+ Op.MSTORE(
248-
32,
249-
Op.CALL(gas=50000, address=invalid_opcode_caller, args_offset=64, args_size=32),
270+
current_opcode.set(opcode)
271+
+ results_sum.add(
272+
Op.CALL(
273+
gas=50000,
274+
address=invalid_opcode_caller,
275+
args_offset=current_opcode.offset,
276+
args_size=32,
277+
),
250278
)
251-
+ Op.MSTORE(0, Op.ADD(Op.MLOAD(0), Op.MLOAD(32)))
252279
for opcode in invalid_opcodes
253280
)
254281
# If any of invalid instructions works, mstore[0] will be > 1
255-
+ Op.MSTORE(0, Op.ADD(Op.MLOAD(0), 1))
256-
+ Op.RETURN(0, 32)
282+
+ results_sum.add(1)
283+
+ results_sum.return_value()
257284
)
258285

259286
return pre.deploy_contract(code=code)

0 commit comments

Comments
 (0)