-
Notifications
You must be signed in to change notification settings - Fork 655
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?
Conversation
@cburgdorf since you started taking a look at Berlin already, do you mind coordinating this review? |
@carver yep, will do. |
@Peppece thanks for kicking this off! I think you could rebase this on top of #1933. This will already give you the basic scaffolding for the
Yep, so far so good 👍
I think the implementation of the new opcodes should go into |
@cburgdorf Awesome! So how do I rebase things on #1933? Do I just copy + paste the code in my branch or is there some git fu I'm missing? |
You can either add my branch as a remote and pull it down or you edit your Then you can simple do:
|
Just in case: after you pull down the other PR, you would literally If you're worried about that, you might want to save a reference to your starting point. I still occasionally find myself doing a: git branch copy-of-branch
git rebase some-branch-i-want-to-add-my-changes-on-top-of
# tool around, write some more code, test it
# take a look around, realize I hate it, decide to blow all changes away since the git branch above
git reset --hard copy-of-branch |
Hey @Peppece just wanted to hear how things are going? Are you still working on this? |
Hey @cburgdorf I was working on this while a school deadline blocked me and I had to focus on that. However, in max 3 days I’ll be back on this. |
@Peppece Sounds good! |
8e8703a
to
92e94c0
Compare
92e94c0
to
e166ab0
Compare
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.
Thanks for working on this.
eth/vm/logic/flow.py
Outdated
|
||
def returnsub(computation: BaseComputation) -> None: | ||
|
||
if computation.rstack.length == 0: |
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.
rather than doing this check explicitely I think it would more closely follow our existing conventions to have the call to computation.rstack_pop1_int()
throw an exception and to do this via a try/except
block.
self.values = values | ||
self._append = values.append | ||
self._pop_typed = values.pop | ||
self.__len__ = values.__len__ |
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.
You might consider using collections.UserList
for this class as I believe it comes built-in with some of the functionality you're doing here.
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.
Though perhaps it's maybe just as effective to subclass list, as the docs seem to recommend:
The need for this class has been partially supplanted by the ability to subclass directly from list; however, this class can be easier to work with because the underlying list is accessible as an attribute.
Not sure there's much benefit to being able to access the underlying list via .data
here, though.
Actually, on second thought, we don't really want/need to expose all the list
methods anyway, so maybe the current approach is better.
eth/vm/rstack.py
Outdated
elif item_type is bytes: | ||
return big_endian_to_int(popped) # type: ignore | ||
else: | ||
raise _busted_type(item_type, popped) |
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.
I don't think this helper function is justified. I think it would better fit our conventions to inline the exception message and raising here.
eth/vm/logic/flow.py
Outdated
|
||
next_opcode = computation.code.peek() | ||
|
||
if next_opcode != BEGINSUB: |
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.
I think you need to first check computation.code.is_valid_opcode(sub_loc)
to ensure that the position is not part of PUSHXX
data, then check if computation.code[sub_loc] == BEGINSUB
, then you can set the program counter to the new position safely.
This prevents leaving the code stream in an awkward invalid position in the event of an invalid JUMPSUB.
I only noticed your review now. Will get back to work on this ASAP. |
It's probably a good idea to rebase against the latest master, to minimize conflicts for yourself. |
91d4b74
to
3960dce
Compare
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.
You should focus on getting the tests passing.
391b4ec
to
e1e988d
Compare
31249be
to
88aad42
Compare
Hello! Is there a particular reason this isn't getting merged? Feel like it has been stuck for a while now. |
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.
Some items remaining:
- Rebase on master
- Drop all the ralexstokes commits
I haven't finished the review, but here are a few thoughts along the way.
computation.code.program_counter) | ||
) | ||
|
||
if computation.code.is_valid_opcode(sub_loc): |
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.
Hm, this valid opcode check looks wrong to me. Ah, interesting, this was not made clear by the spec, IMO. But discussion seems to support the idea that the validity check is required: https://ethereum-magicians.org/t/eip-2315-simple-subroutines-for-the-evm/3941/70?u=carver
So... maybe the only thing to do here is to add an explicit test for JUMPSUB
ing into tho data section of a PUSH
.
eth/vm/logic/flow.py
Outdated
|
||
def jumpsub(computation: BaseComputation) -> None: | ||
sub_loc = computation.stack_pop1_int() | ||
code_range_length = computation.code.__len__() |
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.
__len__()
is a "magic" python function that makes len()
work. So this is preferred:
code_range_length = computation.code.__len__() | |
code_range_length = len(computation.code) |
eth/vm/logic/flow.py
Outdated
if sub_loc >= code_range_length: | ||
raise InvalidJumpDestination( | ||
"Error: at pc={}, op=JUMPSUB: invalid jump destination".format( | ||
computation.code.program_counter) |
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.
Maybe include code_range_length
too?
if sub_op == BEGINSUB: | ||
temp = computation.code.program_counter | ||
computation.code.program_counter = sub_loc + 1 | ||
computation.rstack_push_int(temp) |
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.
Where is the else
block that aborts if the sub_op
is not BEGINSUB
? (and the test that verifies that behavior)
tests/core/opcodes/test_opcodes.py
Outdated
assert comp.get_gas_used() == expect_gas_used | ||
|
||
|
||
@pytest.mark.xfail(reason="invalid subroutines") |
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.
xfail
is better used for things like known bugs. It can hide problems like an incorrect exception instead of a computation that's not successful.
tests/core/opcodes/test_opcodes.py
Outdated
computation.msg, | ||
computation.transaction_context, | ||
) | ||
assert comp.is_success |
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.
Even better here would be to assert that it's a failure, and validate the type of exception that is raised. Typically we would do that with a
assert comp.is_success | |
with pytest.raises(expected_exception): | |
comp.raise_if_error() |
(expected_exception
can be passed in with the test parameters)
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.
Okay, I finished the full pass on this. There's enough going on here that I'll do another pass on it after all these changes are in & tests are green. Feel free to @ me then.
Don't worry about trying to get the ethereum/tests Berlin tests going, it's going to be too tough to run those tests until all of the Berlin EIPs are implemented. But there are a couple more manual tests to add that I mentioned.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think this error message can be more friendly. Something like:
"Error: at pc={}, op=RETURNSUB: invalid retsub".format( | |
"Error: at pc={}, op=RETURNSUB: empty return stack".format( |
# | ||
# Subroutines | ||
# |
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.
Since these opcodes fit in the 0x5_
block nicely, maybe just:
# | |
# Subroutines | |
# | |
# Subroutines |
self.values = values | ||
self._append = values.append | ||
self._pop_typed = values.pop | ||
self.__len__ = values.__len__ |
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.
Though perhaps it's maybe just as effective to subclass list, as the docs seem to recommend:
The need for this class has been partially supplanted by the ability to subclass directly from list; however, this class can be easier to work with because the underlying list is accessible as an attribute.
Not sure there's much benefit to being able to access the underlying list via .data
here, though.
Actually, on second thought, we don't really want/need to expose all the list
methods anyway, so maybe the current approach is better.
__slots__ = ['values', '_append', '_pop_typed', '__len__'] | ||
|
||
def __init__(self) -> None: | ||
values: List[Tuple[type, Union[int, bytes]]] = [] |
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.
Values can't be bytes
anymore.
values: List[Tuple[type, Union[int, bytes]]] = [] | |
values: List[int] = [] |
self._pop_typed = values.pop | ||
self.__len__ = values.__len__ | ||
|
||
def push_int(self, value: int) -> None: |
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.
Since there aren't two type options anymore, this method can just be a push.
def push_int(self, value: int) -> None: | |
def push(self, value: int) -> None: |
VM Return Stack | ||
""" | ||
|
||
__slots__ = ['values', '_append', '_pop_typed', '__len__'] |
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.
__slots__ = ['values', '_append', '_pop_typed', '__len__'] | |
__slots__ = ['values', '_append', '_pop', '__len__'] |
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" | ||
) |
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.
Having a single type lets us simplify a lot, and even do try-first instead of check-first:
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" | |
) | |
try: | |
return self._pop() | |
except IndexError: | |
raise InsufficientStack("Wanted 1 return stack item, had none") |
@pytest.mark.parametrize( | ||
'vm_class, code, expect_gas_used', | ||
( | ||
( |
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.
Would be nice to add a comment here linking to the EIP's test cases:
https://eips.ethereum.org/EIPS/eip-2315#test-cases
( | ||
BerlinVM, | ||
'0x5c5d00', | ||
), |
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.
Another failing test case, that should fail with an InvalidJumpDestination
:
(
BerlinVM,
'0x60035e00',
InvalidJumpDestination,
),
tests/core/opcodes/test_opcodes.py
Outdated
|
||
@pytest.mark.xfail(reason="invalid subroutines") | ||
@pytest.mark.parametrize( | ||
'vm_class, code', |
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.
'vm_class, code', | |
'vm_class, code, expected_exception', |
@@ -140,6 +143,7 @@ def __init__(self, | |||
|
|||
self._memory = Memory() | |||
self._stack = Stack() | |||
self._rstack = RStack() |
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:
self._rstack = RStack() | |
self._return_stack = ReturnStack() |
7fd5d85
to
de0efa5
Compare
Just a heads up that EIP-2315 was pulled from Berlin, so... I guess it makes sense to pause work on this. Sorry friend :/ |
Wow, didn't see this coming. How is this possible? I mean, hasn't this already been implemented for geth and besu? |
@Peppece Yes, it was pulled last minute which is something you wouldn't normaly expect to happen. You may want to read up on the history here: ethereum/pm#263 This doesn't mean your work was for nothing though. There's a chance that the concerns may be addressed and the EIP gets updated / replaced and ships in some other form in an upcoming fork. Sorry, for the disappointing news :/ |
What was wrong?
EIP 2315 introduces three new opcodes (BEGINSUB, JUMPSUB, ENDSUB) which deal with subroutines and needed to be implemented.
How was it fixed?
The opcodes were implemented.
Also, while installing py-evm on a Mac during the project, I noticed that the doc page
contributing.rst
didn't include a step without which py-evm couldn't be installed on the system. I have fixed the doc accordingly.Finally, this also implements the code at #1933 as a basis for the Berlin VM.
Fixes #1926
To-Do
Implement logic in eth/vm/logic
Write tests
Cute Animal Picture
(https://images.pexels.com/photos/146083/pexels-photo-146083.jpeg)