-
Notifications
You must be signed in to change notification settings - Fork 680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP]Implement simple subroutine opcodes #1937
base: main
Are you sure you want to change the base?
Changes from all commits
a16d9a0
de0efa5
b5d98ab
ad402ca
d2e0e4b
279aa26
30ed261
730d4bf
c164746
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,12 +1,15 @@ | ||||||
from eth.exceptions import ( | ||||||
InvalidJumpDestination, | ||||||
InvalidInstruction, | ||||||
OutOfGas, | ||||||
Halt, | ||||||
InsufficientStack, | ||||||
) | ||||||
|
||||||
from eth.vm.computation import BaseComputation | ||||||
from eth.vm.opcode_values import ( | ||||||
JUMPDEST, | ||||||
BEGINSUB, | ||||||
) | ||||||
|
||||||
|
||||||
|
@@ -57,3 +60,49 @@ def gas(computation: BaseComputation) -> None: | |||||
gas_remaining = computation.get_gas_remaining() | ||||||
|
||||||
computation.stack_push_int(gas_remaining) | ||||||
|
||||||
|
||||||
def beginsub(computation: BaseComputation) -> None: | ||||||
raise OutOfGas("Error: at pc={}, op=BEGINSUB: invalid subroutine entry".format( | ||||||
computation.code.program_counter) | ||||||
) | ||||||
|
||||||
|
||||||
def jumpsub(computation: BaseComputation) -> None: | ||||||
sub_loc = computation.stack_pop1_int() | ||||||
code_range_length = len(computation.code) | ||||||
|
||||||
if sub_loc >= code_range_length: | ||||||
raise InvalidJumpDestination( | ||||||
"Error: at pc={}, code_length={}, op=JUMPSUB: invalid jump destination".format( | ||||||
computation.code.program_counter, | ||||||
code_range_length) | ||||||
) | ||||||
|
||||||
if computation.code.is_valid_opcode(sub_loc): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So... maybe the only thing to do here is to add an explicit test for |
||||||
|
||||||
sub_op = computation.code[sub_loc] | ||||||
|
||||||
if sub_op == BEGINSUB: | ||||||
temp = computation.code.program_counter | ||||||
computation.code.program_counter = sub_loc + 1 | ||||||
computation.rstack_push_int(temp) | ||||||
Comment on lines
+86
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where is the |
||||||
|
||||||
else: | ||||||
raise InvalidJumpDestination( | ||||||
"Error: at pc={}, code_length={}, op=JUMPSUB: invalid jump destination".format( | ||||||
computation.code.program_counter, | ||||||
code_range_length) | ||||||
) | ||||||
|
||||||
|
||||||
def returnsub(computation: BaseComputation) -> None: | ||||||
try: | ||||||
ret_loc = computation.rstack_pop1_int() | ||||||
except InsufficientStack: | ||||||
raise InsufficientStack( | ||||||
"Error: at pc={}, op=RETURNSUB: invalid retsub".format( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this error message can be more friendly. Something like:
Suggested change
|
||||||
computation.code.program_counter) | ||||||
) | ||||||
|
||||||
computation.code.program_counter = ret_loc |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -93,6 +93,14 @@ | |||||||||
JUMPDEST = 0x5b | ||||||||||
|
||||||||||
|
||||||||||
# | ||||||||||
# Subroutines | ||||||||||
# | ||||||||||
Comment on lines
+96
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since these opcodes fit in the
Suggested change
|
||||||||||
BEGINSUB = 0x5c | ||||||||||
RETURNSUB = 0x5d | ||||||||||
JUMPSUB = 0x5e | ||||||||||
|
||||||||||
|
||||||||||
# | ||||||||||
# Push Operations | ||||||||||
# | ||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,68 @@ | ||||||||||||||||||||||||||||||||||||
from typing import ( | ||||||||||||||||||||||||||||||||||||
List, | ||||||||||||||||||||||||||||||||||||
Tuple, | ||||||||||||||||||||||||||||||||||||
Union, | ||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
from eth.exceptions import ( | ||||||||||||||||||||||||||||||||||||
InsufficientStack, | ||||||||||||||||||||||||||||||||||||
FullStack, | ||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
from eth.validation import ( | ||||||||||||||||||||||||||||||||||||
validate_stack_int, | ||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
from eth_utils import ( | ||||||||||||||||||||||||||||||||||||
big_endian_to_int, | ||||||||||||||||||||||||||||||||||||
ValidationError, | ||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||
This module simply implements for the return stack the exact same design used for the data stack. | ||||||||||||||||||||||||||||||||||||
As this stack must simply push_int or pop1_int any time a subroutine is accessed or left, only | ||||||||||||||||||||||||||||||||||||
those two functions are provided. | ||||||||||||||||||||||||||||||||||||
For the same reason, the class RStack doesn't inherit from the abc StackAPI, as it would require | ||||||||||||||||||||||||||||||||||||
to implement all the abstract methods defined. | ||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
class RStack: | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's very rare that we use initials in method and class names. A friendlier name here IMO:
Suggested change
|
||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||
VM Return Stack | ||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
__slots__ = ['values', '_append', '_pop_typed', '__len__'] | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
def __init__(self) -> None: | ||||||||||||||||||||||||||||||||||||
values: List[Tuple[type, Union[int, bytes]]] = [] | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Values can't be
Suggested change
|
||||||||||||||||||||||||||||||||||||
self.values = values | ||||||||||||||||||||||||||||||||||||
self._append = values.append | ||||||||||||||||||||||||||||||||||||
self._pop_typed = values.pop | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||
self.__len__ = values.__len__ | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might consider using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though perhaps it's maybe just as effective to subclass list, as the docs seem to recommend:
Not sure there's much benefit to being able to access the underlying list via Actually, on second thought, we don't really want/need to expose all the |
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
def push_int(self, value: int) -> None: | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since there aren't two type options anymore, this method can just be a push.
Suggested change
|
||||||||||||||||||||||||||||||||||||
if len(self.values) > 1023: | ||||||||||||||||||||||||||||||||||||
raise FullStack('Stack limit reached') | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
validate_stack_int(value) | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
self._append((int, value)) | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
def pop1_int(self) -> int: | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||
# | ||||||||||||||||||||||||||||||||||||
# Note: This function is optimized for speed over readability. | ||||||||||||||||||||||||||||||||||||
# | ||||||||||||||||||||||||||||||||||||
if not self.values: | ||||||||||||||||||||||||||||||||||||
raise InsufficientStack("Wanted 1 stack item as int, had none") | ||||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||||
item_type, popped = self._pop_typed() | ||||||||||||||||||||||||||||||||||||
if item_type is int: | ||||||||||||||||||||||||||||||||||||
return popped # type: ignore | ||||||||||||||||||||||||||||||||||||
elif item_type is bytes: | ||||||||||||||||||||||||||||||||||||
return big_endian_to_int(popped) # type: ignore | ||||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||||
raise ValidationError( | ||||||||||||||||||||||||||||||||||||
"Stack must always be bytes or int, " | ||||||||||||||||||||||||||||||||||||
f"got {item_type!r} type" | ||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||
Comment on lines
+56
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having a single type lets us simplify a lot, and even do try-first instead of check-first:
Suggested change
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,9 @@ | |
from eth.exceptions import ( | ||
InvalidInstruction, | ||
VMError, | ||
InvalidJumpDestination, | ||
InsufficientStack, | ||
OutOfGas, | ||
) | ||
from eth.rlp.headers import ( | ||
BlockHeader, | ||
|
@@ -1439,3 +1442,75 @@ def test_blake2b_f_compression(vm_class, input_hex, output_hex, expect_exception | |
comp.raise_if_error() | ||
result = comp.output | ||
assert result.hex() == output_hex | ||
|
||
|
||
@pytest.mark.parametrize( | ||
'vm_class, code, expect_gas_used', | ||
( | ||
( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be nice to add a comment here linking to the EIP's test cases: |
||
BerlinVM, | ||
'0x60045e005c5d', | ||
18, | ||
), | ||
( | ||
BerlinVM, | ||
'0x6800000000000000000c5e005c60115e5d5c5d', | ||
36, | ||
), | ||
( | ||
BerlinVM, | ||
'0x6005565c5d5b60035e', | ||
30, | ||
), | ||
) | ||
) | ||
def test_jumpsub(vm_class, code, expect_gas_used): | ||
computation = setup_computation(vm_class, CANONICAL_ADDRESS_B, decode_hex(code)) | ||
comp = computation.apply_message( | ||
computation.state, | ||
computation.msg, | ||
computation.transaction_context, | ||
) | ||
assert comp.is_success | ||
assert comp.get_gas_used() == expect_gas_used | ||
|
||
|
||
@pytest.mark.parametrize( | ||
'vm_class, code, expected_exception', | ||
( | ||
( | ||
BerlinVM, | ||
'0x5d5858', | ||
InsufficientStack, | ||
), | ||
( | ||
BerlinVM, | ||
'0x6801000000000000000c5e005c60115e5d5c5d', | ||
InvalidJumpDestination, | ||
), | ||
( | ||
BerlinVM, | ||
'0x5c5d00', | ||
OutOfGas, | ||
), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another failing test case, that should fail with an (
BerlinVM,
'0x60035e00',
InvalidJumpDestination,
), |
||
( # tests if the opcode raises error when trying to jump to BEGINSUB into pushdata | ||
BerlinVM, | ||
'0x60055e61005c58', | ||
InvalidJumpDestination, | ||
), | ||
( # tests if the opcode raises error when trying to jump to an opcode other than BEGINGSUB | ||
BerlinVM, | ||
'0x6100055e0058', | ||
InvalidJumpDestination, | ||
) | ||
) | ||
) | ||
def test_failing_jumpsub(vm_class, code, expected_exception): | ||
computation = setup_computation(vm_class, CANONICAL_ADDRESS_B, decode_hex(code)) | ||
comp = computation.apply_message( | ||
computation.state, | ||
computation.msg, | ||
computation.transaction_context, | ||
) | ||
with pytest.raises(expected_exception): | ||
comp.raise_if_error() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, also think that spelling out the full attribute name is friendlier, like: