Skip to content

Commit a16d9a0

Browse files
committed
[WIP] Implement subroutine opcodes
1 parent 509739c commit a16d9a0

File tree

9 files changed

+253
-1
lines changed

9 files changed

+253
-1
lines changed

docs/contributing.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ Thank you for your interest in contributing! We welcome all contributions no mat
66
Setting the stage
77
~~~~~~~~~~~~~~~~~
88

9-
First we need to clone the Py-EVM repository. Py-EVM depends on a submodule of the common tests across all clients, so we need to clone the repo with the ``--recursive`` flag. Example:
9+
**Only on macOS**: if it is the first time you install Py-EVM, you will need to install LevelDB with brew before taking the following steps:
10+
11+
.. code:: sh
12+
13+
brew install python3 LevelDB
14+
15+
16+
17+
We need to clone the Py-EVM repository. Py-EVM depends on a submodule of the common tests across all clients, so we need to clone the repo with the ``--recursive`` flag. Example:
1018

1119
.. code:: sh
1220

eth/abc.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,6 +1655,21 @@ def stack_dup(self, position: int) -> None:
16551655
"""
16561656
...
16571657

1658+
#
1659+
# Return Stack Managemement
1660+
#
1661+
@abstractmethod
1662+
def rstack_push_int(self) -> Callable[[int], None]:
1663+
"""
1664+
Push integer onto the return stack.
1665+
"""
1666+
1667+
@abstractmethod
1668+
def rstack_pop1_int(self) -> Callable[[int], None]:
1669+
"""
1670+
Pop integer off the return stack and return it.
1671+
"""
1672+
16581673
#
16591674
# Computation result
16601675
#

eth/vm/computation.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@
7171
from eth.vm.stack import (
7272
Stack,
7373
)
74+
from eth.vm.rstack import (
75+
RStack,
76+
)
7477

7578

7679
def NO_RESULT(computation: ComputationAPI) -> None:
@@ -140,6 +143,7 @@ def __init__(self,
140143

141144
self._memory = Memory()
142145
self._stack = Stack()
146+
self._rstack = RStack()
143147
self._gas_meter = self.get_gas_meter()
144148

145149
self.children = []
@@ -321,6 +325,17 @@ def stack_push_int(self) -> Callable[[int], None]:
321325
def stack_push_bytes(self) -> Callable[[bytes], None]:
322326
return self._stack.push_bytes
323327

328+
#
329+
# Return Stack Management
330+
#
331+
@cached_property
332+
def rstack_push_int(self) -> Callable[[int], None]:
333+
return self._rstack.push_int
334+
335+
@cached_property
336+
def rstack_pop1_int(self) -> Callable[[], int]:
337+
return self._rstack.pop1_int
338+
324339
#
325340
# Computation result
326341
#

eth/vm/forks/berlin/opcodes.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,37 @@
1717
copy.deepcopy(MUIR_GLACIER_OPCODES),
1818
UPDATED_OPCODES,
1919
)
20+
import copy
21+
22+
from eth_utils.toolz import merge
23+
24+
from eth import constants
25+
from eth.vm import (
26+
mnemonics,
27+
opcode_values,
28+
)
29+
from eth.vm.opcode import as_opcode
30+
from eth.vm.logic import flow
31+
32+
UPDATED_OPCODES = {
33+
opcode_values.BEGINSUB: as_opcode(
34+
logic_fn=flow.beginsub,
35+
mnemonic=mnemonics.BEGINSUB,
36+
gas_cost=constants.GAS_BASE,
37+
),
38+
opcode_values.JUMPSUB: as_opcode(
39+
logic_fn=flow.jumpsub,
40+
mnemonic=mnemonics.JUMPSUB,
41+
gas_cost=constants.GAS_HIGH,
42+
),
43+
opcode_values.RETURNSUB: as_opcode(
44+
logic_fn=flow.returnsub,
45+
mnemonic=mnemonics.RETURNSUB,
46+
gas_cost=constants.GAS_LOW,
47+
),
48+
}
49+
50+
BERLIN_OPCODES = merge(
51+
copy.deepcopy(MUIR_GLACIER_OPCODES),
52+
UPDATED_OPCODES,
53+
)

eth/vm/logic/flow.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from eth.exceptions import (
22
InvalidJumpDestination,
33
InvalidInstruction,
4+
OutOfGas,
45
Halt,
6+
InsufficientStack,
57
)
68

79
from eth.vm.computation import BaseComputation
810
from eth.vm.opcode_values import (
911
JUMPDEST,
12+
BEGINSUB,
1013
)
1114

1215

@@ -57,3 +60,39 @@ def gas(computation: BaseComputation) -> None:
5760
gas_remaining = computation.get_gas_remaining()
5861

5962
computation.stack_push_int(gas_remaining)
63+
64+
65+
def beginsub(computation: BaseComputation) -> None:
66+
raise OutOfGas("Error: at pc={}, op=BEGINSUB: invalid subroutine entry")
67+
68+
69+
def jumpsub(computation: BaseComputation) -> None:
70+
sub_loc = computation.stack_pop1_int()
71+
code_range_length = computation.code.__len__()
72+
73+
if sub_loc >= code_range_length:
74+
raise InvalidJumpDestination(
75+
"Error: at pc={}, op=JUMPSUB: invalid jump destination".format(
76+
computation.code.program_counter)
77+
)
78+
79+
if computation.code.is_valid_opcode(sub_loc):
80+
81+
sub_op = computation.code[sub_loc]
82+
83+
if sub_op == BEGINSUB:
84+
temp = computation.code.program_counter
85+
computation.code.program_counter = sub_loc + 1
86+
computation.rstack_push_int(temp)
87+
88+
89+
def returnsub(computation: BaseComputation) -> None:
90+
try:
91+
ret_loc = computation.rstack_pop1_int()
92+
except InsufficientStack:
93+
raise InsufficientStack(
94+
"Error: at pc={}, op=RETURNSUB: invalid retsub".format(
95+
computation.code.program_counter)
96+
)
97+
98+
computation.code.program_counter = ret_loc

eth/vm/mnemonics.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@
162162
LOG3 = 'LOG3'
163163
LOG4 = 'LOG4'
164164
#
165+
# Subroutines
166+
#
167+
BEGINSUB = 'BEGINSUB'
168+
JUMPSUB = 'JUMPSUB'
169+
RETURNSUB = 'RETURNSUB'
170+
#
165171
# System
166172
#
167173
CREATE = 'CREATE'

eth/vm/opcode_values.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@
9393
JUMPDEST = 0x5b
9494

9595

96+
#
97+
# Subroutines
98+
#
99+
BEGINSUB = 0x5c
100+
RETURNSUB = 0x5d
101+
JUMPSUB = 0x5e
102+
103+
96104
#
97105
# Push Operations
98106
#

eth/vm/rstack.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from typing import (
2+
List,
3+
Tuple,
4+
Union,
5+
)
6+
7+
from eth.exceptions import (
8+
InsufficientStack,
9+
FullStack,
10+
)
11+
12+
from eth.validation import (
13+
validate_stack_int,
14+
)
15+
16+
from eth_utils import (
17+
big_endian_to_int,
18+
ValidationError,
19+
)
20+
21+
"""
22+
This module simply implements for the return stack the exact same design used for the data stack.
23+
As this stack must simply push_int or pop1_int any time a subroutine is accessed or left, only
24+
those two functions are provided.
25+
For the same reason, the class RStack doesn't inherit from the abc StackAPI, as it would require
26+
to implement all the abstract methods defined.
27+
"""
28+
29+
30+
class RStack:
31+
"""
32+
VM Return Stack
33+
"""
34+
35+
__slots__ = ['values', '_append', '_pop_typed', '__len__']
36+
37+
def __init__(self) -> None:
38+
values: List[Tuple[type, Union[int, bytes]]] = []
39+
self.values = values
40+
self._append = values.append
41+
self._pop_typed = values.pop
42+
self.__len__ = values.__len__
43+
44+
def push_int(self, value: int) -> None:
45+
if len(self.values) > 1023:
46+
raise FullStack('Stack limit reached')
47+
48+
validate_stack_int(value)
49+
50+
self._append((int, value))
51+
52+
def pop1_int(self) -> int:
53+
#
54+
# Note: This function is optimized for speed over readability.
55+
#
56+
if not self.values:
57+
raise InsufficientStack("Wanted 1 stack item as int, had none")
58+
else:
59+
item_type, popped = self._pop_typed()
60+
if item_type is int:
61+
return popped # type: ignore
62+
elif item_type is bytes:
63+
return big_endian_to_int(popped) # type: ignore
64+
else:
65+
raise ValidationError(
66+
"Stack must always be bytes or int, "
67+
f"got {item_type!r} type"
68+
)

tests/core/opcodes/test_opcodes.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,3 +1439,62 @@ def test_blake2b_f_compression(vm_class, input_hex, output_hex, expect_exception
14391439
comp.raise_if_error()
14401440
result = comp.output
14411441
assert result.hex() == output_hex
1442+
1443+
1444+
@pytest.mark.parametrize(
1445+
'vm_class, code, expect_gas_used',
1446+
(
1447+
(
1448+
BerlinVM,
1449+
'0x60045e005c5d',
1450+
18,
1451+
),
1452+
(
1453+
BerlinVM,
1454+
'0x6800000000000000000c5e005c60115e5d5c5d',
1455+
36,
1456+
),
1457+
(
1458+
BerlinVM,
1459+
'0x6005565c5d5b60035e',
1460+
30,
1461+
),
1462+
)
1463+
)
1464+
def test_jumpsub(vm_class, code, expect_gas_used):
1465+
computation = setup_computation(vm_class, CANONICAL_ADDRESS_B, decode_hex(code))
1466+
comp = computation.apply_message(
1467+
computation.state,
1468+
computation.msg,
1469+
computation.transaction_context,
1470+
)
1471+
assert comp.is_success
1472+
assert comp.get_gas_used() == expect_gas_used
1473+
1474+
1475+
@pytest.mark.xfail(reason="invalid subroutines")
1476+
@pytest.mark.parametrize(
1477+
'vm_class, code',
1478+
(
1479+
(
1480+
BerlinVM,
1481+
'0x5d5858',
1482+
),
1483+
(
1484+
BerlinVM,
1485+
'0x6801000000000000000c5e005c60115e5d5c5d',
1486+
),
1487+
(
1488+
BerlinVM,
1489+
'0x5c5d00',
1490+
),
1491+
)
1492+
)
1493+
def test_failing_jumpsub(vm_class, code):
1494+
computation = setup_computation(vm_class, CANONICAL_ADDRESS_B, decode_hex(code))
1495+
comp = computation.apply_message(
1496+
computation.state,
1497+
computation.msg,
1498+
computation.transaction_context,
1499+
)
1500+
assert comp.is_success

0 commit comments

Comments
 (0)