Skip to content
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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

Peppece
Copy link

@Peppece Peppece commented May 28, 2020

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)

@carver
Copy link
Contributor

carver commented May 28, 2020

@cburgdorf since you started taking a look at Berlin already, do you mind coordinating this review?

@cburgdorf
Copy link
Contributor

since you started taking a look at Berlin already, do you mind coordinating this review?

@carver yep, will do.

@cburgdorf
Copy link
Contributor

@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 Berlin fork including the BerlinVM which you'll need in test_opcodes.py

I just wanted to know if the mantainers think the changes I made up to this point conform to the style of the project

Yep, so far so good 👍

I also wanted to ask if it was a good idea to create another file inside of the logic directory to implement the logic for the opcodes.

I think the implementation of the new opcodes should go into /eth/vm/logic/flow.py

@Peppece
Copy link
Author

Peppece commented May 29, 2020

@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?

@cburgdorf
Copy link
Contributor

You can either add my branch as a remote and pull it down or you edit your .git/config to always pull down PRs (makes your life much easier).

Then you can simple do:

git fetch origin
git rebase origin/pr/1933

@carver
Copy link
Contributor

carver commented May 29, 2020

Do I just copy + paste the code in my branch or is there some git fu I'm missing?

Just in case: after you pull down the other PR, you would literally git rebase on top of it. (then git push -f) -- notice that these are destructive, and can cause you to lose code, especially if you're still getting comfortable with this tools.

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

@cburgdorf
Copy link
Contributor

Hey @Peppece just wanted to hear how things are going? Are you still working on this?

@Peppece
Copy link
Author

Peppece commented Jun 17, 2020

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.

@cburgdorf
Copy link
Contributor

@Peppece Sounds good!

@Peppece Peppece force-pushed the peppe/implement-subroutine-opcodes branch from 8e8703a to 92e94c0 Compare June 28, 2020 20:43
@Peppece Peppece force-pushed the peppe/implement-subroutine-opcodes branch from 92e94c0 to e166ab0 Compare July 11, 2020 15:32
Copy link
Member

@pipermerriam pipermerriam left a 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.


def returnsub(computation: BaseComputation) -> None:

if computation.rstack.length == 0:
Copy link
Member

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__
Copy link
Member

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.

Copy link
Contributor

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)
Copy link
Member

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.


next_opcode = computation.code.peek()

if next_opcode != BEGINSUB:
Copy link
Member

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.

eth/vm/computation.py Show resolved Hide resolved
@Peppece
Copy link
Author

Peppece commented Sep 16, 2020

I only noticed your review now. Will get back to work on this ASAP.

@carver
Copy link
Contributor

carver commented Sep 16, 2020

It's probably a good idea to rebase against the latest master, to minimize conflicts for yourself.

@Peppece Peppece force-pushed the peppe/implement-subroutine-opcodes branch from 91d4b74 to 3960dce Compare September 19, 2020 16:41
Copy link
Member

@pipermerriam pipermerriam left a 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.

@Peppece Peppece force-pushed the peppe/implement-subroutine-opcodes branch from 391b4ec to e1e988d Compare October 12, 2020 21:15
@Peppece Peppece force-pushed the peppe/implement-subroutine-opcodes branch from 31249be to 88aad42 Compare October 25, 2020 06:58
docs/contributing.rst Outdated Show resolved Hide resolved
@Peppece Peppece requested a review from carver November 3, 2020 17:41
@Peppece
Copy link
Author

Peppece commented Nov 17, 2020

Hello! Is there a particular reason this isn't getting merged? Feel like it has been stuck for a while now.

Copy link
Contributor

@carver carver left a 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.

eth/vm/logic/flow.py Outdated Show resolved Hide resolved
computation.code.program_counter)
)

if computation.code.is_valid_opcode(sub_loc):
Copy link
Contributor

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 JUMPSUBing into tho data section of a PUSH.


def jumpsub(computation: BaseComputation) -> None:
sub_loc = computation.stack_pop1_int()
code_range_length = computation.code.__len__()
Copy link
Contributor

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:

Suggested change
code_range_length = computation.code.__len__()
code_range_length = len(computation.code)

if sub_loc >= code_range_length:
raise InvalidJumpDestination(
"Error: at pc={}, op=JUMPSUB: invalid jump destination".format(
computation.code.program_counter)
Copy link
Contributor

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?

Comment on lines +83 to +86
if sub_op == BEGINSUB:
temp = computation.code.program_counter
computation.code.program_counter = sub_loc + 1
computation.rstack_push_int(temp)
Copy link
Contributor

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)

assert comp.get_gas_used() == expect_gas_used


@pytest.mark.xfail(reason="invalid subroutines")
Copy link
Contributor

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.

computation.msg,
computation.transaction_context,
)
assert comp.is_success
Copy link
Contributor

@carver carver Feb 23, 2021

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

Suggested change
assert comp.is_success
with pytest.raises(expected_exception):
comp.raise_if_error()

(expected_exception can be passed in with the test parameters)

Copy link
Contributor

@carver carver left a 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(
Copy link
Contributor

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:

Suggested change
"Error: at pc={}, op=RETURNSUB: invalid retsub".format(
"Error: at pc={}, op=RETURNSUB: empty return stack".format(

Comment on lines +96 to +98
#
# Subroutines
#
Copy link
Contributor

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:

Suggested change
#
# Subroutines
#
# Subroutines

self.values = values
self._append = values.append
self._pop_typed = values.pop
self.__len__ = values.__len__
Copy link
Contributor

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]]] = []
Copy link
Contributor

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.

Suggested change
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:
Copy link
Contributor

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.

Suggested change
def push_int(self, value: int) -> None:
def push(self, value: int) -> None:

VM Return Stack
"""

__slots__ = ['values', '_append', '_pop_typed', '__len__']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
__slots__ = ['values', '_append', '_pop_typed', '__len__']
__slots__ = ['values', '_append', '_pop', '__len__']

Comment on lines +56 to +68
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"
)
Copy link
Contributor

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:

Suggested change
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',
(
(
Copy link
Contributor

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',
),
Copy link
Contributor

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,
        ),


@pytest.mark.xfail(reason="invalid subroutines")
@pytest.mark.parametrize(
'vm_class, code',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'vm_class, code',
'vm_class, code, expected_exception',

@@ -140,6 +143,7 @@ def __init__(self,

self._memory = Memory()
self._stack = Stack()
self._rstack = RStack()
Copy link
Contributor

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:

Suggested change
self._rstack = RStack()
self._return_stack = ReturnStack()

@Peppece Peppece force-pushed the peppe/implement-subroutine-opcodes branch from 7fd5d85 to de0efa5 Compare March 9, 2021 22:43
@carver
Copy link
Contributor

carver commented Mar 11, 2021

Just a heads up that EIP-2315 was pulled from Berlin, so... I guess it makes sense to pause work on this. Sorry friend :/

@Peppece
Copy link
Author

Peppece commented Mar 11, 2021

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?

@cburgdorf
Copy link
Contributor

@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 :/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for simple subroutines (EIP-2315, scheduled for Berlin)
4 participants