diff --git a/docs/contributing.rst b/docs/contributing.rst index 115a32665e..8ffe99d4ef 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -6,7 +6,10 @@ Thank you for your interest in contributing! We welcome all contributions no mat Setting the stage ~~~~~~~~~~~~~~~~~ -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: +.. note:: + If it is the first time you install py-evm on macOS, check https://py-evm.readthedocs.io/en/latest/guides/quickstart.html#installing-on-macos + +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: .. code:: sh diff --git a/docs/guides/quickstart.rst b/docs/guides/quickstart.rst index 5f9319e05d..8c42deb851 100644 --- a/docs/guides/quickstart.rst +++ b/docs/guides/quickstart.rst @@ -37,11 +37,11 @@ Finally, we can install the ``py-evm`` package via pip. Installing on macOS ------------------- -First, install Python 3 with brew: +First, install Python 3 and LevelDB with brew: .. code:: sh - brew install python3 + brew install python3 leveldb .. note:: .. include:: /fragments/virtualenv_explainer.rst diff --git a/eth/abc.py b/eth/abc.py index ff08ec0792..c5535d3128 100644 --- a/eth/abc.py +++ b/eth/abc.py @@ -1655,6 +1655,21 @@ def stack_dup(self, position: int) -> None: """ ... + # + # Return Stack Managemement + # + @abstractmethod + def rstack_push_int(self) -> Callable[[int], None]: + """ + Push integer onto the return stack. + """ + + @abstractmethod + def rstack_pop1_int(self) -> Callable[[int], None]: + """ + Pop integer off the return stack and return it. + """ + # # Computation result # diff --git a/eth/exceptions.py b/eth/exceptions.py index 28de8bd6d4..e82f663c62 100644 --- a/eth/exceptions.py +++ b/eth/exceptions.py @@ -117,7 +117,7 @@ class OutOfGas(VMError): class InsufficientStack(VMError): """ - Raised when the stack is empty. + Raised when the stack or the return stack is empty. """ pass @@ -131,7 +131,7 @@ class FullStack(VMError): class InvalidJumpDestination(VMError): """ - Raised when the jump destination for a JUMPDEST operation is invalid. + Raised when the jump destination for a JUMPDEST or JUMPSUB operation is invalid. """ pass diff --git a/eth/vm/computation.py b/eth/vm/computation.py index 5b601591fb..fbfcfa7b4c 100644 --- a/eth/vm/computation.py +++ b/eth/vm/computation.py @@ -71,6 +71,9 @@ from eth.vm.stack import ( Stack, ) +from eth.vm.rstack import ( + RStack, +) def NO_RESULT(computation: ComputationAPI) -> None: @@ -140,6 +143,7 @@ def __init__(self, self._memory = Memory() self._stack = Stack() + self._rstack = RStack() self._gas_meter = self.get_gas_meter() self.children = [] @@ -321,6 +325,17 @@ def stack_push_int(self) -> Callable[[int], None]: def stack_push_bytes(self) -> Callable[[bytes], None]: return self._stack.push_bytes + # + # Return Stack Management + # + @cached_property + def rstack_push_int(self) -> Callable[[int], None]: + return self._rstack.push_int + + @cached_property + def rstack_pop1_int(self) -> Callable[[], int]: + return self._rstack.pop1_int + # # Computation result # diff --git a/eth/vm/forks/berlin/opcodes.py b/eth/vm/forks/berlin/opcodes.py index 9ea87cbd99..92bf3f8028 100644 --- a/eth/vm/forks/berlin/opcodes.py +++ b/eth/vm/forks/berlin/opcodes.py @@ -8,6 +8,14 @@ ) from eth.vm.opcode import Opcode +from eth import constants + +from eth.vm import ( + mnemonics, + opcode_values, +) +from eth.vm.opcode import as_opcode +from eth.vm.logic import flow UPDATED_OPCODES: Dict[int, Opcode] = { # New opcodes @@ -17,3 +25,27 @@ copy.deepcopy(MUIR_GLACIER_OPCODES), UPDATED_OPCODES, ) + + +UPDATED_OPCODES = { + opcode_values.BEGINSUB: as_opcode( + logic_fn=flow.beginsub, + mnemonic=mnemonics.BEGINSUB, + gas_cost=constants.GAS_BASE, + ), + opcode_values.JUMPSUB: as_opcode( + logic_fn=flow.jumpsub, + mnemonic=mnemonics.JUMPSUB, + gas_cost=constants.GAS_HIGH, + ), + opcode_values.RETURNSUB: as_opcode( + logic_fn=flow.returnsub, + mnemonic=mnemonics.RETURNSUB, + gas_cost=constants.GAS_LOW, + ), +} + +BERLIN_OPCODES = merge( + copy.deepcopy(MUIR_GLACIER_OPCODES), + UPDATED_OPCODES, +) diff --git a/eth/vm/logic/flow.py b/eth/vm/logic/flow.py index 47fc7a15ad..83e775eac1 100644 --- a/eth/vm/logic/flow.py +++ b/eth/vm/logic/flow.py @@ -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): + + 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) + + 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( + computation.code.program_counter) + ) + + computation.code.program_counter = ret_loc diff --git a/eth/vm/mnemonics.py b/eth/vm/mnemonics.py index 4d83de24a1..88361d550c 100644 --- a/eth/vm/mnemonics.py +++ b/eth/vm/mnemonics.py @@ -162,6 +162,12 @@ LOG3 = 'LOG3' LOG4 = 'LOG4' # +# Subroutines +# +BEGINSUB = 'BEGINSUB' +JUMPSUB = 'JUMPSUB' +RETURNSUB = 'RETURNSUB' +# # System # CREATE = 'CREATE' diff --git a/eth/vm/opcode_values.py b/eth/vm/opcode_values.py index 2845e7d4d7..26fc77d411 100644 --- a/eth/vm/opcode_values.py +++ b/eth/vm/opcode_values.py @@ -93,6 +93,14 @@ JUMPDEST = 0x5b +# +# Subroutines +# +BEGINSUB = 0x5c +RETURNSUB = 0x5d +JUMPSUB = 0x5e + + # # Push Operations # diff --git a/eth/vm/rstack.py b/eth/vm/rstack.py new file mode 100644 index 0000000000..3316e3dffe --- /dev/null +++ b/eth/vm/rstack.py @@ -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: + """ + VM Return Stack + """ + + __slots__ = ['values', '_append', '_pop_typed', '__len__'] + + def __init__(self) -> None: + values: List[Tuple[type, Union[int, bytes]]] = [] + self.values = values + self._append = values.append + self._pop_typed = values.pop + self.__len__ = values.__len__ + + def push_int(self, value: int) -> None: + if len(self.values) > 1023: + raise FullStack('Stack limit reached') + + validate_stack_int(value) + + self._append((int, value)) + + def pop1_int(self) -> int: + # + # 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" + ) diff --git a/tests/core/opcodes/test_opcodes.py b/tests/core/opcodes/test_opcodes.py index aa9ce10adf..c63ef292ec 100644 --- a/tests/core/opcodes/test_opcodes.py +++ b/tests/core/opcodes/test_opcodes.py @@ -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', + ( + ( + 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, + ), + ( # 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()