From e00cb59dbb4960cfab312e8976bf43d75873cfb2 Mon Sep 17 00:00:00 2001 From: Somu Bhargava Date: Mon, 28 Jun 2021 15:35:58 +0530 Subject: [PATCH 1/3] Implement EVM arithmetic operations and Push-N operations --- src/ethereum/base_types.py | 64 +++- src/ethereum/utils.py | 26 ++ src/ethereum/vm/__init__.py | 1 + src/ethereum/vm/gas.py | 3 + src/ethereum/vm/instructions.py | 398 ++++++++++++++++++++++++- src/ethereum/vm/interpreter.py | 6 +- src/ethereum/vm/ops.py | 101 ++++++- tests/test_base_types.py | 44 ++- tests/vm/__init__.py | 0 tests/vm/test_arithmetic_operations.py | 365 +++++++++++++++++++++++ whitelist.txt | 4 + 11 files changed, 980 insertions(+), 32 deletions(-) create mode 100644 src/ethereum/utils.py create mode 100644 tests/vm/__init__.py create mode 100644 tests/vm/test_arithmetic_operations.py diff --git a/src/ethereum/base_types.py b/src/ethereum/base_types.py index 6bd7fea5500..5e43bffd925 100644 --- a/src/ethereum/base_types.py +++ b/src/ethereum/base_types.py @@ -18,6 +18,11 @@ from typing import Optional, Tuple, Type +U255_MAX_VALUE = (2 ** 255) - 1 +U255_CEIL_VALUE = 2 ** 255 +U256_MAX_VALUE = (2 ** 256) - 1 +U256_CEIL_VALUE = 2 ** 256 + class Uint(int): """ @@ -280,11 +285,31 @@ def from_be_bytes(cls: Type, buffer: "Bytes") -> "U256": return cls(int.from_bytes(buffer, "big")) + @classmethod + def from_signed(cls: Type, value: int) -> "U256": + """ + Converts a signed number into a 256-bit unsigned integer. + + Parameters + ---------- + value : + Signed number + + Returns + ------- + self : `U256` + Unsigned integer obtained from `value`. + """ + if value >= 0: + return cls(value) + + return cls(value & U256_MAX_VALUE) + def __new__(cls: Type, value: int) -> "U256": if not isinstance(value, int): raise TypeError() - if value < 0 or value >= 2 ** 256: + if value < 0 or value > U256_MAX_VALUE: raise ValueError() return super(cls, cls).__new__(cls, value) @@ -315,7 +340,8 @@ def wrapping_add(self, right: int) -> "U256": if result == NotImplemented: return NotImplemented - result %= 2 ** 256 + # This is a fast way of ensuring that the result is < (2 ** 256) + result &= self.MAX_VALUE return self.__class__(result) def __iadd__(self, right: int) -> "U256": @@ -344,7 +370,8 @@ def wrapping_sub(self, right: int) -> "U256": if result == NotImplemented: return NotImplemented - result %= 2 ** 256 + # This is a fast way of ensuring that the result is < (2 ** 256) + result &= self.MAX_VALUE return self.__class__(result) def __rsub__(self, left: int) -> "U256": @@ -375,7 +402,8 @@ def wrapping_mul(self, right: int) -> "U256": if result == NotImplemented: return NotImplemented - result %= 2 ** 256 + # This is a fast way of ensuring that the result is < (2 ** 256) + result &= self.MAX_VALUE return self.__class__(result) def __mul__(self, right: int) -> "U256": @@ -432,9 +460,6 @@ def __rmod__(self, left: int) -> "U256": if not isinstance(left, int): return NotImplemented - if left < 0 or left > self.MAX_VALUE: - raise ValueError() - result = super(U256, self).__rmod__(left) return self.__class__(result) @@ -466,7 +491,7 @@ def unchecked_pow(self, right: int, modulo: Optional[int] = None) -> int: if not isinstance(modulo, int): return NotImplemented - if modulo < 0 or modulo > self.MAX_VALUE: + if modulo < 0 or modulo > U256_CEIL_VALUE: raise ValueError() if not isinstance(right, int): @@ -483,7 +508,8 @@ def wrapping_pow(self, right: int, modulo: Optional[int] = None) -> "U256": if result == NotImplemented: return NotImplemented - result %= 2 ** 256 + # This is a fast way of ensuring that the result is < (2 ** 256) + result &= self.MAX_VALUE return self.__class__(result) def __pow__(self, right: int, modulo: Optional[int] = None) -> "U256": @@ -499,7 +525,7 @@ def __rpow__(self, left: int, modulo: Optional[int] = None) -> "U256": if not isinstance(modulo, int): return NotImplemented - if modulo < 0 or modulo > self.MAX_VALUE: + if modulo < 0 or modulo > U256_CEIL_VALUE: raise ValueError() if not isinstance(left, int): @@ -542,8 +568,24 @@ def to_be_bytes(self) -> "Bytes": byte_length = (bit_length + 7) // 8 return self.to_bytes(byte_length, "big") + def to_signed(self) -> int: + """ + Converts this 256-bit unsigned integer into a signed integer. + + Returns + ------- + signed_int : `int` + Signed integer obtained from 256-bit unsigned integer. + """ + if self <= U255_MAX_VALUE: + # This means that the sign bit is 0 + return int(self) + + # -1 * (2's complement of U256 value) + return int(self) - U256_CEIL_VALUE + -U256.MAX_VALUE = U256(2 ** 256 - 1) +U256.MAX_VALUE = U256(U256_MAX_VALUE) Bytes = bytes diff --git a/src/ethereum/utils.py b/src/ethereum/utils.py new file mode 100644 index 00000000000..d3656141b0c --- /dev/null +++ b/src/ethereum/utils.py @@ -0,0 +1,26 @@ +""" +Utility functions used in this application. +""" + + +def get_sign(value: int) -> int: + """ + Determines the sign of a number. + + Parameters + ---------- + value : `int` + The value whose sign is to be determined. + + Returns + ------- + sign : `int` + The sign of the number (-1 or 0 or 1). + The return value is based on math signum function. + """ + if value < 0: + return -1 + elif value == 0: + return 0 + else: + return 1 diff --git a/src/ethereum/vm/__init__.py b/src/ethereum/vm/__init__.py index 487a2867a77..e57d6ac0131 100644 --- a/src/ethereum/vm/__init__.py +++ b/src/ethereum/vm/__init__.py @@ -55,3 +55,4 @@ class Evm: depth: Uint env: Environment refund_counter: Uint + running: bool diff --git a/src/ethereum/vm/gas.py b/src/ethereum/vm/gas.py index 488df51999b..29b34212056 100644 --- a/src/ethereum/vm/gas.py +++ b/src/ethereum/vm/gas.py @@ -19,6 +19,9 @@ GAS_STORAGE_SET = U256(20000) GAS_STORAGE_UPDATE = U256(5000) GAS_STORAGE_CLEAR_REFUND = U256(15000) +GAS_LOW = U256(5) +GAS_MID = U256(8) +GAS_EXPONENTIATION = U256(10) def subtract_gas(gas_left: U256, amount: U256) -> U256: diff --git a/src/ethereum/vm/instructions.py b/src/ethereum/vm/instructions.py index 994364f3aa3..f6d5ef7f9de 100644 --- a/src/ethereum/vm/instructions.py +++ b/src/ethereum/vm/instructions.py @@ -13,9 +13,16 @@ """ -from ..base_types import U256 +from functools import partial +from typing import cast + +from ..base_types import U255_CEIL_VALUE, U256, U256_MAX_VALUE +from ..utils import get_sign from . import Evm from .gas import ( + GAS_EXPONENTIATION, + GAS_LOW, + GAS_MID, GAS_STORAGE_CLEAR_REFUND, GAS_STORAGE_SET, GAS_STORAGE_UPDATE, @@ -25,6 +32,23 @@ from .stack import pop, push +def stop(evm: Evm) -> None: + """ + Stop further execution of EVM code. + + Parameters + ---------- + evm : + The current EVM frame. + """ + evm.running = False + + +# +# Arithmetic Operations +# + + def add(evm: Evm) -> None: """ Adds the top two elements of the stack together, and pushes the result back @@ -46,10 +70,323 @@ def add(evm: Evm) -> None: x = pop(evm.stack) y = pop(evm.stack) + result = x.wrapping_add(y) + + push(evm.stack, result) + + +def sub(evm: Evm) -> None: + """ + Subtracts the top two elements of the stack, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + x = pop(evm.stack) + y = pop(evm.stack) + result = x.wrapping_sub(y) + + push(evm.stack, result) + + +def mul(evm: Evm) -> None: + """ + Multiply the top two elements of the stack, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `GAS_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + x = pop(evm.stack) + y = pop(evm.stack) + result = x.wrapping_mul(y) + + push(evm.stack, result) + + +def div(evm: Evm) -> None: + """ + Integer division of the top two elements of the stack. Pushes the result + back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `GAS_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + dividend = pop(evm.stack) + divisor = pop(evm.stack) + if divisor == 0: + quotient = U256(0) + else: + quotient = dividend // divisor + + push(evm.stack, quotient) + + +def sdiv(evm: Evm) -> None: + """ + Signed integer division of the top two elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `GAS_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + dividend = pop(evm.stack).to_signed() + divisor = pop(evm.stack).to_signed() + + if divisor == 0: + quotient = 0 + elif dividend == -U255_CEIL_VALUE and divisor == -1: + quotient = -U255_CEIL_VALUE + else: + sign = get_sign(dividend * divisor) + quotient = sign * (abs(dividend) // abs(divisor)) + + push(evm.stack, U256.from_signed(quotient)) + + +def mod(evm: Evm) -> None: + """ + Modulo remainder of the top two elements of the stack. Pushes the result + back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `GAS_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + x = pop(evm.stack) + y = pop(evm.stack) + if y == 0: + remainder = U256(0) + else: + remainder = x % y + + push(evm.stack, remainder) + + +def smod(evm: Evm) -> None: + """ + Signed modulo remainder of the top two elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `GAS_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + x = pop(evm.stack).to_signed() + y = pop(evm.stack).to_signed() + + if y == 0: + remainder = 0 + else: + remainder = get_sign(x) * (abs(x) % abs(y)) + + push(evm.stack, U256.from_signed(remainder)) + + +def addmod(evm: Evm) -> None: + """ + Modulo addition of the top 2 elements with the 3rd element. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `3`. + OutOfGasError + If `evm.gas_left` is less than `GAS_MID`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_MID) + + x = pop(evm.stack) + y = pop(evm.stack) + z = pop(evm.stack) + + if z == 0: + result = U256(0) + else: + result = x.unchecked_add(y) % z + + push(evm.stack, result) + + +def mulmod(evm: Evm) -> None: + """ + Modulo multiplication of the top 2 elements with the 3rd element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `3`. + OutOfGasError + If `evm.gas_left` is less than `GAS_MID`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_MID) + + x = pop(evm.stack) + y = pop(evm.stack) + z = pop(evm.stack) + + if z == 0: + result = U256(0) + else: + result = x.unchecked_mul(y) % z + + push(evm.stack, result) + + +def exp(evm: Evm) -> None: + """ + Exponential operation of the top 2 elements. Pushes the result back on + the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `GAS_MID`. + """ + base = pop(evm.stack) + exponent = pop(evm.stack) - val = x.wrapping_add(y) + gas_used = GAS_EXPONENTIATION + if exponent != 0: + # This is equivalent to 1 + floor(log(y, 256)). But in python the log + # function is inaccurate leading to wrong results. + exponent_bits = exponent.bit_length() + exponent_bytes = (exponent_bits + 7) // 8 + gas_used += GAS_EXPONENTIATION * exponent_bytes + evm.gas_left = subtract_gas(evm.gas_left, gas_used) - push(evm.stack, val) + result = cast(U256, pow(base, exponent, U256_MAX_VALUE + 1)) + + push(evm.stack, result) + + +def signextend(evm: Evm) -> None: + """ + Sign extend operation. In other words, extend a signed number which + fits in N bytes to 32 bytes. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `GAS_LOW`. + """ + evm.gas_left = subtract_gas(evm.gas_left, GAS_LOW) + + # byte_num would be 0-indexed when inserted to the stack. + byte_num = pop(evm.stack) + value = pop(evm.stack) + + if byte_num > 31: + # Can't extend any further + result = value + else: + # U256(0).to_be_bytes() gives b'' instead b'\x00'. # noqa: SC100 + value_bytes = value.to_be_bytes() or b"\x00" + + # Now among the obtained value bytes, consider only + # N `least significant bytes`, where N is `byte_num + 1`. + value_bytes = value_bytes[len(value_bytes) - 1 - byte_num :] + sign_bit = value_bytes[0] >> 7 + if sign_bit == 0: + result = U256.from_be_bytes(value_bytes) + else: + num_bytes_prepend = 32 - (byte_num + 1) + result = U256.from_be_bytes( + bytearray([0xFF] * num_bytes_prepend) + value_bytes + ) + + push(evm.stack, result) def sstore(evm: Evm) -> None: @@ -88,20 +425,25 @@ def sstore(evm: Evm) -> None: evm.refund_counter += GAS_STORAGE_CLEAR_REFUND if new_value == 0: - del evm.env.state[evm.current].storage[key] + # Deletes a k-v pair from dict if key is present, else does nothing + evm.env.state[evm.current].storage.pop(key, None) else: evm.env.state[evm.current].storage[key] = new_value -def push1(evm: Evm) -> None: +def push_n(evm: Evm, num_bytes: int) -> None: """ - Pushes a one-byte immediate onto the stack. + Pushes a N-byte immediate onto the stack. Parameters ---------- evm : The current EVM frame. + num_bytes : `int` + The number of immediate bytes to be read from the code and pushed to + the stack. + Raises ------ StackOverflowError @@ -109,6 +451,46 @@ def push1(evm: Evm) -> None: OutOfGasError If `evm.gas_left` is less than `GAS_VERY_LOW`. """ + assert evm.pc + num_bytes < len(evm.code) evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) - push(evm.stack, U256(evm.code[evm.pc + 1])) - evm.pc += 1 + + data_to_push = U256.from_be_bytes( + evm.code[evm.pc + 1 : evm.pc + num_bytes + 1] + ) + push(evm.stack, data_to_push) + + evm.pc += num_bytes + + +push1 = partial(push_n, num_bytes=1) +push2 = partial(push_n, num_bytes=2) +push3 = partial(push_n, num_bytes=3) +push4 = partial(push_n, num_bytes=4) +push5 = partial(push_n, num_bytes=5) +push6 = partial(push_n, num_bytes=6) +push7 = partial(push_n, num_bytes=7) +push8 = partial(push_n, num_bytes=8) +push9 = partial(push_n, num_bytes=9) +push10 = partial(push_n, num_bytes=10) +push11 = partial(push_n, num_bytes=11) +push12 = partial(push_n, num_bytes=12) +push13 = partial(push_n, num_bytes=13) +push14 = partial(push_n, num_bytes=14) +push15 = partial(push_n, num_bytes=15) +push16 = partial(push_n, num_bytes=16) +push17 = partial(push_n, num_bytes=17) +push18 = partial(push_n, num_bytes=18) +push19 = partial(push_n, num_bytes=19) +push20 = partial(push_n, num_bytes=20) +push21 = partial(push_n, num_bytes=21) +push22 = partial(push_n, num_bytes=22) +push23 = partial(push_n, num_bytes=23) +push24 = partial(push_n, num_bytes=24) +push25 = partial(push_n, num_bytes=25) +push26 = partial(push_n, num_bytes=26) +push27 = partial(push_n, num_bytes=27) +push28 = partial(push_n, num_bytes=28) +push29 = partial(push_n, num_bytes=29) +push30 = partial(push_n, num_bytes=30) +push31 = partial(push_n, num_bytes=31) +push32 = partial(push_n, num_bytes=32) diff --git a/src/ethereum/vm/interpreter.py b/src/ethereum/vm/interpreter.py index 8b051555a17..0f13de08e8c 100644 --- a/src/ethereum/vm/interpreter.py +++ b/src/ethereum/vm/interpreter.py @@ -75,6 +75,7 @@ def process_call( depth=depth, env=env, refund_counter=Uint(0), + running=True, ) logs: List[Log] = [] @@ -83,11 +84,14 @@ def process_call( evm.env.state[evm.caller].balance -= evm.value evm.env.state[evm.current].balance += evm.value - while evm.pc < len(evm.code): + while evm.running: op = evm.code[evm.pc] op_implementation[op](evm) evm.pc += 1 + if evm.pc >= len(evm.code): + evm.running = False + gas_used = gas - evm.gas_left refund = min(gas_used // 2, evm.refund_counter) diff --git a/src/ethereum/vm/ops.py b/src/ethereum/vm/ops.py index 0c0a83584c5..d979e0fb77f 100644 --- a/src/ethereum/vm/ops.py +++ b/src/ethereum/vm/ops.py @@ -13,14 +13,105 @@ implementations. """ -from .instructions import add, push1, sstore +from typing import Callable, Dict +from . import instructions + +# Arithmetic Operations +STOP = 0x00 ADD = 0x01 +MUL = 0x02 +SUB = 0x03 +DIV = 0x04 +SDIV = 0x05 +MOD = 0x06 +SMOD = 0x07 +ADDMOD = 0x08 +MULMOD = 0x09 +EXP = 0x0A +SIGNEXTEND = 0x0B + +# Push Operations PUSH1 = 0x60 +PUSH2 = 0x61 +PUSH3 = 0x62 +PUSH4 = 0x63 +PUSH5 = 0x64 +PUSH6 = 0x65 +PUSH7 = 0x66 +PUSH8 = 0x67 +PUSH9 = 0x68 +PUSH10 = 0x69 +PUSH11 = 0x6A +PUSH12 = 0x6B +PUSH13 = 0x6C +PUSH14 = 0x6D +PUSH15 = 0x6E +PUSH16 = 0x6F +PUSH17 = 0x70 +PUSH18 = 0x71 +PUSH19 = 0x72 +PUSH20 = 0x73 +PUSH21 = 0x74 +PUSH22 = 0x75 +PUSH23 = 0x76 +PUSH24 = 0x77 +PUSH25 = 0x78 +PUSH26 = 0x79 +PUSH27 = 0x7A +PUSH28 = 0x7B +PUSH29 = 0x7C +PUSH30 = 0x7D +PUSH31 = 0x7E +PUSH32 = 0x7F + SSTORE = 0x55 -op_implementation = { - ADD: add, - SSTORE: sstore, - PUSH1: push1, + +op_implementation: Dict[int, Callable] = { + STOP: instructions.stop, + ADD: instructions.add, + MUL: instructions.mul, + SUB: instructions.sub, + DIV: instructions.div, + SDIV: instructions.sdiv, + MOD: instructions.mod, + SMOD: instructions.smod, + ADDMOD: instructions.addmod, + MULMOD: instructions.mulmod, + EXP: instructions.exp, + SIGNEXTEND: instructions.signextend, + SSTORE: instructions.sstore, + PUSH1: instructions.push1, + PUSH2: instructions.push2, + PUSH3: instructions.push3, + PUSH4: instructions.push4, + PUSH5: instructions.push5, + PUSH6: instructions.push6, + PUSH7: instructions.push7, + PUSH8: instructions.push8, + PUSH9: instructions.push9, + PUSH10: instructions.push10, + PUSH11: instructions.push11, + PUSH12: instructions.push12, + PUSH13: instructions.push13, + PUSH14: instructions.push14, + PUSH15: instructions.push15, + PUSH16: instructions.push16, + PUSH17: instructions.push17, + PUSH18: instructions.push18, + PUSH19: instructions.push19, + PUSH20: instructions.push20, + PUSH21: instructions.push21, + PUSH22: instructions.push22, + PUSH23: instructions.push23, + PUSH24: instructions.push24, + PUSH25: instructions.push25, + PUSH26: instructions.push26, + PUSH27: instructions.push27, + PUSH28: instructions.push28, + PUSH29: instructions.push29, + PUSH30: instructions.push30, + PUSH31: instructions.push31, + PUSH32: instructions.push32, } diff --git a/tests/test_base_types.py b/tests/test_base_types.py index 244ab3732a0..76cc0361450 100644 --- a/tests/test_base_types.py +++ b/tests/test_base_types.py @@ -1,6 +1,6 @@ import pytest -from ethereum.base_types import U256, Uint +from ethereum.base_types import U256, U256_MAX_VALUE, Uint def test_uint_new() -> None: @@ -857,8 +857,14 @@ def test_u256_rmod() -> None: def test_u256_rmod_negative() -> None: - with pytest.raises(ValueError): - (-4) % U256(5) + value = (-4) % U256(5) + assert value == 1 + + +def test_u256_rmod_left_more_than_max_u256() -> None: + value = (2 ** 257) % U256(U256_MAX_VALUE) + assert isinstance(value, U256) + assert value == 2 def test_u256_rmod_float() -> None: @@ -989,9 +995,15 @@ def test_u256_pow_modulo() -> None: assert value == 1 +def test_u256_pow_max_modulo() -> None: + value = pow(U256(U256_MAX_VALUE), U256_MAX_VALUE, 2 ** 256) + assert isinstance(value, U256) + assert value == U256_MAX_VALUE + + def test_u256_pow_modulo_overflow() -> None: with pytest.raises(ValueError): - pow(U256(4), 2, 2 ** 256) + pow(U256(4), 2, 2 ** 257) def test_u256_pow_modulo_negative() -> None: @@ -1021,9 +1033,15 @@ def test_u256_rpow_modulo() -> None: assert value == 1 +def test_u256_rpow_max_modulo() -> None: + value = U256.__rpow__(U256(U256_MAX_VALUE), U256_MAX_VALUE, 2 ** 256) + assert isinstance(value, U256) + assert value == U256_MAX_VALUE + + def test_u256_rpow_modulo_overflow() -> None: with pytest.raises(ValueError): - U256.__rpow__(U256(2), 4, 2 ** 256) + U256.__rpow__(U256(2), 4, 2 ** 256 + 1) def test_u256_rpow_modulo_negative() -> None: @@ -1061,9 +1079,15 @@ def test_u256_ipow_modulo_negative() -> None: U256(4).__ipow__(2, -3) +def test_u256_ipow_max_modulo() -> None: + value = U256(U256_MAX_VALUE).__ipow__(U256_MAX_VALUE, 2 ** 256) + assert isinstance(value, U256) + assert value == U256_MAX_VALUE + + def test_u256_ipow_modulo_overflow() -> None: with pytest.raises(ValueError): - U256(4).__ipow__(2, 2 ** 256) + U256(4).__ipow__(2, 2 ** 256 + 1) def test_u256_wrapping_pow() -> None: @@ -1089,9 +1113,15 @@ def test_u256_wrapping_pow_modulo() -> None: assert value == 1 +def test_u256_wrapping_pow_max_modulo() -> None: + value = U256(U256_MAX_VALUE).wrapping_pow(U256_MAX_VALUE, 2 ** 256) + assert isinstance(value, U256) + assert value == U256_MAX_VALUE + + def test_u256_wrapping_pow_modulo_overflow() -> None: with pytest.raises(ValueError): - U256(4).wrapping_pow(2, 2 ** 256) + U256(4).wrapping_pow(2, 2 ** 256 + 1) def test_u256_wrapping_pow_modulo_negative() -> None: diff --git a/tests/vm/__init__.py b/tests/vm/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/vm/test_arithmetic_operations.py b/tests/vm/test_arithmetic_operations.py new file mode 100644 index 00000000000..c9499d57f1d --- /dev/null +++ b/tests/vm/test_arithmetic_operations.py @@ -0,0 +1,365 @@ +import json +import os +from typing import Any + +import pytest + +from ethereum import rlp +from ethereum.base_types import U256, Uint +from ethereum.crypto import keccak256 +from ethereum.eth_types import Account, State +from ethereum.spec import print_state +from ethereum.vm import Environment +from ethereum.vm.interpreter import process_call +from tests.helpers import ( + hex2address, + hex2bytes, + hex2bytes32, + hex2u256, + hex2uint, +) + + +@pytest.mark.parametrize( + "test_file", + [ + "add0.json", + "add1.json", + "add2.json", + "add3.json", + "add4.json", + ], +) +def test_add(test_file: str) -> None: + run_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "sub0.json", + "sub1.json", + "sub2.json", + "sub3.json", + "sub4.json", + ], +) +def test_sub(test_file: str) -> None: + run_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "mul0.json", + "mul1.json", + "mul2.json", + "mul3.json", + "mul4.json", + "mul5.json", + "mul6.json", + # TODO: Uncomment mul7.json once MLOAD, MSTORE is implemented + # "mul7.json", + ], +) +def test_mul(test_file: str) -> None: + run_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + # TODO: Uncomment div1.json file once MSTORE is implemented + # "div1.json", + "divBoostBug.json", + "divByNonZero0.json", + "divByNonZero1.json", + "divByNonZero2.json", + "divByNonZero3.json", + "divByZero.json", + "divByZero_2.json", + ], +) +def test_div(test_file: str) -> None: + run_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "sdiv0.json", + "sdiv1.json", + "sdiv2.json", + "sdiv3.json", + "sdiv4.json", + "sdiv5.json", + "sdiv6.json", + "sdiv7.json", + "sdiv8.json", + "sdiv9.json", + "sdivByZero0.json", + "sdivByZero1.json", + "sdivByZero2.json", + "sdiv_i256min.json", + "sdiv_i256min2.json", + "sdiv_i256min3.json", + # TODO: Run sdiv_dejavu.json once DUP series has been implemented + # "sdiv_dejavu.json", + ], +) +def test_sdiv(test_file: str) -> None: + run_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "mod0.json", + "mod1.json", + "mod2.json", + "mod3.json", + "mod4.json", + "modByZero.json", + ], +) +def test_mod(test_file: str) -> None: + run_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "smod0.json", + "smod1.json", + "smod2.json", + "smod3.json", + "smod4.json", + "smod5.json", + "smod6.json", + "smod7.json", + "smod8_byZero.json", + "smod_i256min1.json", + "smod_i256min2.json", + ], +) +def test_smod(test_file: str) -> None: + run_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "addmod0.json", + "addmod1.json", + "addmod1_overflow2.json", + "addmod1_overflow3.json", + "addmod1_overflow4.json", + "addmod1_overflowDiff.json", + "addmod2.json", + # TODO: Test files 'addmod2_1.json', 'addmod3_0.json' after implementing EQ + # TODO: Test file 'addmod2_0.json' after implementing EQ + # "addmod2_0.json", + # "addmod2_1.json", + "addmod3.json", + # "addmod3_0.json", + ], +) +def test_addmod(test_file: str) -> None: + run_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "mulmod0.json", + "mulmod1.json", + "mulmod1_overflow.json", + "mulmod1_overflow2.json", + "mulmod1_overflow3.json", + "mulmod1_overflow4.json", + "mulmod2.json", + # TODO: Test files 'mulmod2_1.json', 'mulmod3_0.json' after implementing EQ + # TODO: Test file 'mulmod2_0.json' after implementing SMOD + # TODO: Test file 'mulmod4.json' after implementing MSTORE8 + # "mulmod2_0.json", + # "mulmod2_1.json", + "mulmod3.json", + # "mulmod3_0.json", + # "mulmod4.json", + ], +) +def test_mulmod(test_file: str) -> None: + run_test(test_file) + + +@pytest.mark.parametrize( + "test_file", + [ + "exp0.json", + "exp1.json", + "exp2.json", + "exp3.json", + "exp4.json", + "exp5.json", + "exp6.json", + "exp7.json", + "exp8.json", + # TODO: Run expXY.json, expXY_success.json when CALLDATALOAD is implemented + # "expXY.json", + # "expXY_success.json", + ], +) +def test_exp(test_file: str) -> None: + run_test(test_file) + + +@pytest.mark.parametrize("exponent", ([2, 4, 8, 16, 32, 64, 128, 256])) +def test_exp_power_2(exponent: int) -> None: + run_test(f"expPowerOf2_{exponent}.json") + + +def test_exp_power_256() -> None: + for i in range(1, 34): + run_test(f"expPowerOf256_{i}.json") + + for i in range(34): + run_test(f"expPowerOf256Of256_{i}.json") + + +@pytest.mark.parametrize( + "test_file", + [ + "signextend_0_BigByte.json", + "signextend_00.json", + "signextend_AlmostBiggestByte.json", + "signextend_BigByte_0.json", + "signextend_BigByteBigByte.json", + "signextend_BigBytePlus1_2.json", + "signextend_bigBytePlus1.json", + "signextend_BitIsNotSet.json", + "signextend_BitIsNotSetInHigherByte.json", + "signextend_bitIsSet.json", + "signextend_BitIsSetInHigherByte.json", + # TODO: Run the below commented test after implementing JUMP opcode + # "signextend_Overflow_dj42.json", + "signextendInvalidByteNumber.json", + ], +) +def test_signextend(test_file: str) -> None: + run_test(test_file) + + +def test_stop() -> None: + run_test("stop.json") + + +# +# Test helpers +# +def run_test(test_file: str) -> None: + test_data = load_test(test_file) + target = test_data["target"] + env = test_data["env"] + + gas_left, logs = process_call( + caller=test_data["caller"], + target=target, + data=test_data["data"], + value=test_data["value"], + gas=test_data["gas"], + depth=test_data["depth"], + env=env, + ) + + assert gas_left == test_data["expected_gas_left"] + assert keccak256(rlp.encode(logs)) == test_data["expected_logs_hash"] + # We are checking only the storage here and not the whole state, as the + # balances in the testcases don't change even though some value is + # transferred along with code invokation. But our evm execution transfers + # the value as well as executing the code. + assert ( + env.state[target].storage + == test_data["expected_post_state"][target].storage + ) + + +def load_test(test_file: str) -> Any: + test_name = os.path.splitext(test_file)[0] + path = os.path.join( + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmArithmeticTest/", + test_file, + ) + with open(path, "r") as fp: + json_data = json.load(fp)[test_name] + + env = json_to_env(json_data) + + return { + "caller": hex2address(json_data["exec"]["caller"]), + "target": hex2address(json_data["exec"]["address"]), + "data": hex2bytes(json_data["exec"]["data"]), + "value": hex2u256(json_data["exec"]["value"]), + "gas": hex2u256(json_data["exec"]["gas"]), + "depth": Uint(0), + "env": env, + "expected_gas_left": hex2u256(json_data["gas"]), + "expected_logs_hash": hex2bytes(json_data["logs"]), + "expected_post_state": json_to_state(json_data["post"]), + } + + +def json_to_env(json_data: Any) -> Environment: + caller_hex_address = json_data["exec"]["caller"] + # Some tests don't have the caller state defined in the test case. Hence + # creating a dummy caller state. + if caller_hex_address not in json_data["pre"]: + value = json_data["exec"]["value"] + json_data["pre"][caller_hex_address] = get_dummy_account_state(value) + + current_state = json_to_state(json_data["pre"]) + + return Environment( + caller=hex2address(json_data["exec"]["caller"]), + origin=hex2address(json_data["exec"]["origin"]), + block_hashes=[], + coinbase=hex2address(json_data["env"]["currentCoinbase"]), + number=hex2uint(json_data["env"]["currentNumber"]), + gas_limit=hex2uint(json_data["env"]["currentGasLimit"]), + gas_price=hex2u256(json_data["exec"]["gasPrice"]), + time=hex2u256(json_data["env"]["currentTimestamp"]), + difficulty=hex2uint(json_data["env"]["currentDifficulty"]), + state=current_state, + ) + + +def json_to_state(raw: Any) -> State: + state = {} + for (addr, acc_state) in raw.items(): + account = Account( + nonce=hex2uint(acc_state.get("nonce", "0x0")), + balance=hex2uint(acc_state.get("balance", "0x0")), + code=hex2bytes(acc_state.get("code", "")), + storage={}, + ) + + for (k, v) in acc_state.get("storage", {}).items(): + account.storage[hex2bytes32(k)] = U256.from_be_bytes( + hex2bytes32(v) + ) + + state[hex2address(addr)] = account + + return state + + +def get_dummy_account_state(min_balance: str) -> Any: + # dummy account balance is the min balance needed plus 1 eth for gas + # cost + account_balance = hex2uint(min_balance) + (10 ** 18) + + return { + "balance": hex(account_balance), + "code": "", + "nonce": "0x00", + "storage": {}, + } diff --git a/whitelist.txt b/whitelist.txt index 7f96d3f4dc5..1ea97fa4f3e 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -26,6 +26,7 @@ patricialize rlp trie U256 +U255 secp256k1 secp256k1n iadd @@ -54,6 +55,9 @@ preimage substring klass vm +num +utils +prepend sha3 From 9fcf071822089b14a8950cd3482ae3e8d2152202 Mon Sep 17 00:00:00 2001 From: Somu Bhargava Date: Fri, 2 Jul 2021 16:10:03 +0530 Subject: [PATCH 2/3] Implement DupN and SwapN operations --- src/ethereum/vm/instructions.py | 91 +++++++++++++- src/ethereum/vm/ops.py | 68 ++++++++++ tests/vm/test_arithmetic_operations.py | 164 ++++--------------------- tests/vm/test_push_dup_swap.py | 57 +++++++++ tests/vm/vm_test_helpers.py | 123 +++++++++++++++++++ 5 files changed, 359 insertions(+), 144 deletions(-) create mode 100644 tests/vm/test_push_dup_swap.py create mode 100644 tests/vm/vm_test_helpers.py diff --git a/src/ethereum/vm/instructions.py b/src/ethereum/vm/instructions.py index f6d5ef7f9de..dc9afcdf9dc 100644 --- a/src/ethereum/vm/instructions.py +++ b/src/ethereum/vm/instructions.py @@ -440,7 +440,7 @@ def push_n(evm: Evm, num_bytes: int) -> None: evm : The current EVM frame. - num_bytes : `int` + num_bytes : The number of immediate bytes to be read from the code and pushed to the stack. @@ -462,6 +462,61 @@ def push_n(evm: Evm, num_bytes: int) -> None: evm.pc += num_bytes +def dup_n(evm: Evm, item_number: int) -> None: + """ + Duplicate the Nth stack item (from top of the stack) to the top of stack. + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be duplicated + to the top of stack. + + Raises + ------ + OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + assert item_number < len(evm.stack) + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + data_to_duplicate = evm.stack[len(evm.stack) - 1 - item_number] + push(evm.stack, data_to_duplicate) + + +def swap_n(evm: Evm, item_number: int) -> None: + """ + Swap the 1st and Nth items in the stack. All items are 0-indexed from the + top of the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be swapped + with the top of stack element. + + Raises + ------ + OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + assert item_number < len(evm.stack) + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + top_element_idx = len(evm.stack) - 1 + nth_element_idx = len(evm.stack) - 1 - item_number + evm.stack[top_element_idx], evm.stack[nth_element_idx] = ( + evm.stack[nth_element_idx], + evm.stack[top_element_idx], + ) + + push1 = partial(push_n, num_bytes=1) push2 = partial(push_n, num_bytes=2) push3 = partial(push_n, num_bytes=3) @@ -494,3 +549,37 @@ def push_n(evm: Evm, num_bytes: int) -> None: push30 = partial(push_n, num_bytes=30) push31 = partial(push_n, num_bytes=31) push32 = partial(push_n, num_bytes=32) + +dup1 = partial(dup_n, item_number=0) +dup2 = partial(dup_n, item_number=1) +dup3 = partial(dup_n, item_number=2) +dup4 = partial(dup_n, item_number=3) +dup5 = partial(dup_n, item_number=4) +dup6 = partial(dup_n, item_number=5) +dup7 = partial(dup_n, item_number=6) +dup8 = partial(dup_n, item_number=7) +dup9 = partial(dup_n, item_number=8) +dup10 = partial(dup_n, item_number=9) +dup11 = partial(dup_n, item_number=10) +dup12 = partial(dup_n, item_number=11) +dup13 = partial(dup_n, item_number=12) +dup14 = partial(dup_n, item_number=13) +dup15 = partial(dup_n, item_number=14) +dup16 = partial(dup_n, item_number=15) + +swap1 = partial(swap_n, item_number=1) +swap2 = partial(swap_n, item_number=2) +swap3 = partial(swap_n, item_number=3) +swap4 = partial(swap_n, item_number=4) +swap5 = partial(swap_n, item_number=5) +swap6 = partial(swap_n, item_number=6) +swap7 = partial(swap_n, item_number=7) +swap8 = partial(swap_n, item_number=8) +swap9 = partial(swap_n, item_number=9) +swap10 = partial(swap_n, item_number=10) +swap11 = partial(swap_n, item_number=11) +swap12 = partial(swap_n, item_number=12) +swap13 = partial(swap_n, item_number=13) +swap14 = partial(swap_n, item_number=14) +swap15 = partial(swap_n, item_number=15) +swap16 = partial(swap_n, item_number=16) diff --git a/src/ethereum/vm/ops.py b/src/ethereum/vm/ops.py index d979e0fb77f..d27e1c2f4a1 100644 --- a/src/ethereum/vm/ops.py +++ b/src/ethereum/vm/ops.py @@ -65,6 +65,42 @@ PUSH31 = 0x7E PUSH32 = 0x7F +# Dup operations +DUP1 = 0x80 +DUP2 = 0x81 +DUP3 = 0x82 +DUP4 = 0x83 +DUP5 = 0x84 +DUP6 = 0x85 +DUP7 = 0x86 +DUP8 = 0x87 +DUP9 = 0x88 +DUP10 = 0x89 +DUP11 = 0x8A +DUP12 = 0x8B +DUP13 = 0x8C +DUP14 = 0x8D +DUP15 = 0x8E +DUP16 = 0x8F + +# Swap operations +SWAP1 = 0x90 +SWAP2 = 0x91 +SWAP3 = 0x92 +SWAP4 = 0x93 +SWAP5 = 0x94 +SWAP6 = 0x95 +SWAP7 = 0x96 +SWAP8 = 0x97 +SWAP9 = 0x98 +SWAP10 = 0x99 +SWAP11 = 0x9A +SWAP12 = 0x9B +SWAP13 = 0x9C +SWAP14 = 0x9D +SWAP15 = 0x9E +SWAP16 = 0x9F + SSTORE = 0x55 @@ -114,4 +150,36 @@ PUSH30: instructions.push30, PUSH31: instructions.push31, PUSH32: instructions.push32, + DUP1: instructions.dup1, + DUP2: instructions.dup2, + DUP3: instructions.dup3, + DUP4: instructions.dup4, + DUP5: instructions.dup5, + DUP6: instructions.dup6, + DUP7: instructions.dup7, + DUP8: instructions.dup8, + DUP9: instructions.dup9, + DUP10: instructions.dup10, + DUP11: instructions.dup11, + DUP12: instructions.dup12, + DUP13: instructions.dup13, + DUP14: instructions.dup14, + DUP15: instructions.dup15, + DUP16: instructions.dup16, + SWAP1: instructions.swap1, + SWAP2: instructions.swap2, + SWAP3: instructions.swap3, + SWAP4: instructions.swap4, + SWAP5: instructions.swap5, + SWAP6: instructions.swap6, + SWAP7: instructions.swap7, + SWAP8: instructions.swap8, + SWAP9: instructions.swap9, + SWAP10: instructions.swap10, + SWAP11: instructions.swap11, + SWAP12: instructions.swap12, + SWAP13: instructions.swap13, + SWAP14: instructions.swap14, + SWAP15: instructions.swap15, + SWAP16: instructions.swap16, } diff --git a/tests/vm/test_arithmetic_operations.py b/tests/vm/test_arithmetic_operations.py index c9499d57f1d..b9e239ad13b 100644 --- a/tests/vm/test_arithmetic_operations.py +++ b/tests/vm/test_arithmetic_operations.py @@ -1,22 +1,12 @@ -import json -import os -from typing import Any +from functools import partial import pytest -from ethereum import rlp -from ethereum.base_types import U256, Uint -from ethereum.crypto import keccak256 -from ethereum.eth_types import Account, State -from ethereum.spec import print_state -from ethereum.vm import Environment -from ethereum.vm.interpreter import process_call -from tests.helpers import ( - hex2address, - hex2bytes, - hex2bytes32, - hex2u256, - hex2uint, +from tests.vm.vm_test_helpers import run_test + +run_arithmetic_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmArithmeticTest", ) @@ -31,7 +21,7 @@ ], ) def test_add(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) @pytest.mark.parametrize( @@ -45,7 +35,7 @@ def test_add(test_file: str) -> None: ], ) def test_sub(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) @pytest.mark.parametrize( @@ -63,7 +53,7 @@ def test_sub(test_file: str) -> None: ], ) def test_mul(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) @pytest.mark.parametrize( @@ -81,7 +71,7 @@ def test_mul(test_file: str) -> None: ], ) def test_div(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) @pytest.mark.parametrize( @@ -108,7 +98,7 @@ def test_div(test_file: str) -> None: ], ) def test_sdiv(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) @pytest.mark.parametrize( @@ -123,7 +113,7 @@ def test_sdiv(test_file: str) -> None: ], ) def test_mod(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) @pytest.mark.parametrize( @@ -143,7 +133,7 @@ def test_mod(test_file: str) -> None: ], ) def test_smod(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) @pytest.mark.parametrize( @@ -165,7 +155,7 @@ def test_smod(test_file: str) -> None: ], ) def test_addmod(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) @pytest.mark.parametrize( @@ -189,7 +179,7 @@ def test_addmod(test_file: str) -> None: ], ) def test_mulmod(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) @pytest.mark.parametrize( @@ -210,20 +200,20 @@ def test_mulmod(test_file: str) -> None: ], ) def test_exp(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) @pytest.mark.parametrize("exponent", ([2, 4, 8, 16, 32, 64, 128, 256])) def test_exp_power_2(exponent: int) -> None: - run_test(f"expPowerOf2_{exponent}.json") + run_arithmetic_vm_test(f"expPowerOf2_{exponent}.json") def test_exp_power_256() -> None: for i in range(1, 34): - run_test(f"expPowerOf256_{i}.json") + run_arithmetic_vm_test(f"expPowerOf256_{i}.json") for i in range(34): - run_test(f"expPowerOf256Of256_{i}.json") + run_arithmetic_vm_test(f"expPowerOf256Of256_{i}.json") @pytest.mark.parametrize( @@ -246,120 +236,8 @@ def test_exp_power_256() -> None: ], ) def test_signextend(test_file: str) -> None: - run_test(test_file) + run_arithmetic_vm_test(test_file) def test_stop() -> None: - run_test("stop.json") - - -# -# Test helpers -# -def run_test(test_file: str) -> None: - test_data = load_test(test_file) - target = test_data["target"] - env = test_data["env"] - - gas_left, logs = process_call( - caller=test_data["caller"], - target=target, - data=test_data["data"], - value=test_data["value"], - gas=test_data["gas"], - depth=test_data["depth"], - env=env, - ) - - assert gas_left == test_data["expected_gas_left"] - assert keccak256(rlp.encode(logs)) == test_data["expected_logs_hash"] - # We are checking only the storage here and not the whole state, as the - # balances in the testcases don't change even though some value is - # transferred along with code invokation. But our evm execution transfers - # the value as well as executing the code. - assert ( - env.state[target].storage - == test_data["expected_post_state"][target].storage - ) - - -def load_test(test_file: str) -> Any: - test_name = os.path.splitext(test_file)[0] - path = os.path.join( - "tests/fixtures/LegacyTests/Constantinople/VMTests/vmArithmeticTest/", - test_file, - ) - with open(path, "r") as fp: - json_data = json.load(fp)[test_name] - - env = json_to_env(json_data) - - return { - "caller": hex2address(json_data["exec"]["caller"]), - "target": hex2address(json_data["exec"]["address"]), - "data": hex2bytes(json_data["exec"]["data"]), - "value": hex2u256(json_data["exec"]["value"]), - "gas": hex2u256(json_data["exec"]["gas"]), - "depth": Uint(0), - "env": env, - "expected_gas_left": hex2u256(json_data["gas"]), - "expected_logs_hash": hex2bytes(json_data["logs"]), - "expected_post_state": json_to_state(json_data["post"]), - } - - -def json_to_env(json_data: Any) -> Environment: - caller_hex_address = json_data["exec"]["caller"] - # Some tests don't have the caller state defined in the test case. Hence - # creating a dummy caller state. - if caller_hex_address not in json_data["pre"]: - value = json_data["exec"]["value"] - json_data["pre"][caller_hex_address] = get_dummy_account_state(value) - - current_state = json_to_state(json_data["pre"]) - - return Environment( - caller=hex2address(json_data["exec"]["caller"]), - origin=hex2address(json_data["exec"]["origin"]), - block_hashes=[], - coinbase=hex2address(json_data["env"]["currentCoinbase"]), - number=hex2uint(json_data["env"]["currentNumber"]), - gas_limit=hex2uint(json_data["env"]["currentGasLimit"]), - gas_price=hex2u256(json_data["exec"]["gasPrice"]), - time=hex2u256(json_data["env"]["currentTimestamp"]), - difficulty=hex2uint(json_data["env"]["currentDifficulty"]), - state=current_state, - ) - - -def json_to_state(raw: Any) -> State: - state = {} - for (addr, acc_state) in raw.items(): - account = Account( - nonce=hex2uint(acc_state.get("nonce", "0x0")), - balance=hex2uint(acc_state.get("balance", "0x0")), - code=hex2bytes(acc_state.get("code", "")), - storage={}, - ) - - for (k, v) in acc_state.get("storage", {}).items(): - account.storage[hex2bytes32(k)] = U256.from_be_bytes( - hex2bytes32(v) - ) - - state[hex2address(addr)] = account - - return state - - -def get_dummy_account_state(min_balance: str) -> Any: - # dummy account balance is the min balance needed plus 1 eth for gas - # cost - account_balance = hex2uint(min_balance) + (10 ** 18) - - return { - "balance": hex(account_balance), - "code": "", - "nonce": "0x00", - "storage": {}, - } + run_arithmetic_vm_test("stop.json") diff --git a/tests/vm/test_push_dup_swap.py b/tests/vm/test_push_dup_swap.py new file mode 100644 index 00000000000..1431ce88af7 --- /dev/null +++ b/tests/vm/test_push_dup_swap.py @@ -0,0 +1,57 @@ +from functools import partial + +import pytest + +from tests.vm.vm_test_helpers import run_test + +run_push_vm_test = partial( + run_test, + "tests/fixtures/LegacyTests/Constantinople/VMTests/vmPushDupSwapTest", +) +run_dup_vm_test = run_swap_vm_test = run_push_vm_test + + +def test_push_successfully() -> None: + for i in range(1, 34): + run_push_vm_test(f"push{i}.json") + + run_push_vm_test("push32Undefined2.json") + # TODO: Run below test once suicide opcode has been implemented + # "push32AndSuicide.json" + + +@pytest.mark.parametrize( + "test_file", + [ + "push1_missingStack.json", + "push32Undefined.json", + "push32Undefined3.json", + "push32FillUpInputWithZerosAtTheEnd.json", + ], +) +def test_push_failed(test_file: str) -> None: + with pytest.raises(AssertionError): + run_push_vm_test(test_file) + + +def test_dup() -> None: + for i in range(1, 17): + run_dup_vm_test(f"dup{i}.json") + + +def test_dup_error() -> None: + with pytest.raises(AssertionError): + run_dup_vm_test("dup2error.json") + + +def test_swap() -> None: + for i in range(1, 17): + run_swap_vm_test(f"swap{i}.json") + + # TODO: Run below test once JUMP opcode has been implemented + # "swapjump1.json" + + +def test_swap_error() -> None: + with pytest.raises(AssertionError): + run_swap_vm_test("swap2error.json") diff --git a/tests/vm/vm_test_helpers.py b/tests/vm/vm_test_helpers.py new file mode 100644 index 00000000000..b9142490e44 --- /dev/null +++ b/tests/vm/vm_test_helpers.py @@ -0,0 +1,123 @@ +import json +import os +from typing import Any + +from ethereum import rlp +from ethereum.base_types import U256, Uint +from ethereum.crypto import keccak256 +from ethereum.eth_types import Account, State +from ethereum.vm import Environment +from ethereum.vm.interpreter import process_call +from tests.helpers import ( + hex2address, + hex2bytes, + hex2bytes32, + hex2u256, + hex2uint, +) + + +def run_test(test_dir: str, test_file: str) -> None: + test_data = load_test(test_dir, test_file) + target = test_data["target"] + env = test_data["env"] + + gas_left, logs = process_call( + caller=test_data["caller"], + target=target, + data=test_data["data"], + value=test_data["value"], + gas=test_data["gas"], + depth=test_data["depth"], + env=env, + ) + + assert gas_left == test_data["expected_gas_left"] + assert keccak256(rlp.encode(logs)) == test_data["expected_logs_hash"] + # We are checking only the storage here and not the whole state, as the + # balances in the testcases don't change even though some value is + # transferred along with code invokation. But our evm execution transfers + # the value as well as executing the code. + assert ( + env.state[target].storage + == test_data["expected_post_state"][target].storage + ) + + +def load_test(test_dir: str, test_file: str) -> Any: + test_name = os.path.splitext(test_file)[0] + path = os.path.join(test_dir, test_file) + with open(path, "r") as fp: + json_data = json.load(fp)[test_name] + + env = json_to_env(json_data) + + return { + "caller": hex2address(json_data["exec"]["caller"]), + "target": hex2address(json_data["exec"]["address"]), + "data": hex2bytes(json_data["exec"]["data"]), + "value": hex2u256(json_data["exec"]["value"]), + "gas": hex2u256(json_data["exec"]["gas"]), + "depth": Uint(0), + "env": env, + "expected_gas_left": hex2u256(json_data.get("gas", "0x64")), + "expected_logs_hash": hex2bytes(json_data.get("logs", "0x00")), + "expected_post_state": json_to_state(json_data.get("post", {})), + } + + +def json_to_env(json_data: Any) -> Environment: + caller_hex_address = json_data["exec"]["caller"] + # Some tests don't have the caller state defined in the test case. Hence + # creating a dummy caller state. + if caller_hex_address not in json_data["pre"]: + value = json_data["exec"]["value"] + json_data["pre"][caller_hex_address] = get_dummy_account_state(value) + + current_state = json_to_state(json_data["pre"]) + + return Environment( + caller=hex2address(json_data["exec"]["caller"]), + origin=hex2address(json_data["exec"]["origin"]), + block_hashes=[], + coinbase=hex2address(json_data["env"]["currentCoinbase"]), + number=hex2uint(json_data["env"]["currentNumber"]), + gas_limit=hex2uint(json_data["env"]["currentGasLimit"]), + gas_price=hex2u256(json_data["exec"]["gasPrice"]), + time=hex2u256(json_data["env"]["currentTimestamp"]), + difficulty=hex2uint(json_data["env"]["currentDifficulty"]), + state=current_state, + ) + + +def json_to_state(raw: Any) -> State: + state = {} + for (addr, acc_state) in raw.items(): + account = Account( + nonce=hex2uint(acc_state.get("nonce", "0x0")), + balance=hex2uint(acc_state.get("balance", "0x0")), + code=hex2bytes(acc_state.get("code", "")), + storage={}, + ) + + for (k, v) in acc_state.get("storage", {}).items(): + account.storage[hex2bytes32(k)] = U256.from_be_bytes( + hex2bytes32(v) + ) + + state[hex2address(addr)] = account + + return state + + +def get_dummy_account_state(min_balance: str) -> Any: + # dummy account balance is the min balance needed plus 1 eth for gas + # cost + account_balance = hex2uint(min_balance) + (10 ** 18) + + return { + "balance": hex(account_balance), + "code": "", + "nonce": "0x00", + "storage": {}, + } From 1c1273ee16713d9c43b0e4fc5e377e85fc2281eb Mon Sep 17 00:00:00 2001 From: Somu Bhargava Date: Wed, 7 Jul 2021 14:06:43 +0530 Subject: [PATCH 3/3] Convert opcodes to Enums --- setup.cfg | 2 +- src/ethereum/vm/instructions/__init__.py | 198 +++++++++++++++ .../arithmetic.py} | 234 +----------------- src/ethereum/vm/instructions/computation.py | 27 ++ src/ethereum/vm/instructions/stack.py | 174 +++++++++++++ src/ethereum/vm/instructions/storage.py | 65 +++++ src/ethereum/vm/interpreter.py | 4 +- src/ethereum/vm/ops.py | 185 -------------- 8 files changed, 475 insertions(+), 414 deletions(-) create mode 100644 src/ethereum/vm/instructions/__init__.py rename src/ethereum/vm/{instructions.py => instructions/arithmetic.py} (54%) create mode 100644 src/ethereum/vm/instructions/computation.py create mode 100644 src/ethereum/vm/instructions/stack.py create mode 100644 src/ethereum/vm/instructions/storage.py delete mode 100644 src/ethereum/vm/ops.py diff --git a/setup.cfg b/setup.cfg index 42c54560e25..1028ac8c683 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,7 @@ classifiers = License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication [options] -packages = ethereum, ethereum/vm +packages = ethereum, ethereum/vm, ethereum/vm/instructions package_dir = =src diff --git a/src/ethereum/vm/instructions/__init__.py b/src/ethereum/vm/instructions/__init__.py new file mode 100644 index 00000000000..685bc525bbc --- /dev/null +++ b/src/ethereum/vm/instructions/__init__.py @@ -0,0 +1,198 @@ +""" +EVM Instruction Encoding (Opcodes) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Machine readable representations of EVM instructions, and a mapping to their +implementations. +""" + +import enum +from typing import Callable, Dict + +from . import arithmetic as arithmetic_instructions +from . import computation as computation_instructions +from . import stack as stack_instructions +from . import storage as storage_instructions + + +class Ops(enum.Enum): + """ + Enum for EVM Opcodes + """ + + # Arithmetic Ops + ADD = 0x01 + MUL = 0x02 + SUB = 0x03 + DIV = 0x04 + SDIV = 0x05 + MOD = 0x06 + SMOD = 0x07 + ADDMOD = 0x08 + MULMOD = 0x09 + EXP = 0x0A + SIGNEXTEND = 0x0B + + # Computation Ops + STOP = 0x00 + + # Storage Ops + SSTORE = 0x55 + + # Push Operations + PUSH1 = 0x60 + PUSH2 = 0x61 + PUSH3 = 0x62 + PUSH4 = 0x63 + PUSH5 = 0x64 + PUSH6 = 0x65 + PUSH7 = 0x66 + PUSH8 = 0x67 + PUSH9 = 0x68 + PUSH10 = 0x69 + PUSH11 = 0x6A + PUSH12 = 0x6B + PUSH13 = 0x6C + PUSH14 = 0x6D + PUSH15 = 0x6E + PUSH16 = 0x6F + PUSH17 = 0x70 + PUSH18 = 0x71 + PUSH19 = 0x72 + PUSH20 = 0x73 + PUSH21 = 0x74 + PUSH22 = 0x75 + PUSH23 = 0x76 + PUSH24 = 0x77 + PUSH25 = 0x78 + PUSH26 = 0x79 + PUSH27 = 0x7A + PUSH28 = 0x7B + PUSH29 = 0x7C + PUSH30 = 0x7D + PUSH31 = 0x7E + PUSH32 = 0x7F + + # Dup operations + DUP1 = 0x80 + DUP2 = 0x81 + DUP3 = 0x82 + DUP4 = 0x83 + DUP5 = 0x84 + DUP6 = 0x85 + DUP7 = 0x86 + DUP8 = 0x87 + DUP9 = 0x88 + DUP10 = 0x89 + DUP11 = 0x8A + DUP12 = 0x8B + DUP13 = 0x8C + DUP14 = 0x8D + DUP15 = 0x8E + DUP16 = 0x8F + + # Swap operations + SWAP1 = 0x90 + SWAP2 = 0x91 + SWAP3 = 0x92 + SWAP4 = 0x93 + SWAP5 = 0x94 + SWAP6 = 0x95 + SWAP7 = 0x96 + SWAP8 = 0x97 + SWAP9 = 0x98 + SWAP10 = 0x99 + SWAP11 = 0x9A + SWAP12 = 0x9B + SWAP13 = 0x9C + SWAP14 = 0x9D + SWAP15 = 0x9E + SWAP16 = 0x9F + + +op_implementation: Dict[Ops, Callable] = { + Ops.STOP: computation_instructions.stop, + Ops.ADD: arithmetic_instructions.add, + Ops.MUL: arithmetic_instructions.mul, + Ops.SUB: arithmetic_instructions.sub, + Ops.DIV: arithmetic_instructions.div, + Ops.SDIV: arithmetic_instructions.sdiv, + Ops.MOD: arithmetic_instructions.mod, + Ops.SMOD: arithmetic_instructions.smod, + Ops.ADDMOD: arithmetic_instructions.addmod, + Ops.MULMOD: arithmetic_instructions.mulmod, + Ops.EXP: arithmetic_instructions.exp, + Ops.SIGNEXTEND: arithmetic_instructions.signextend, + Ops.SSTORE: storage_instructions.sstore, + Ops.PUSH1: stack_instructions.push1, + Ops.PUSH2: stack_instructions.push2, + Ops.PUSH3: stack_instructions.push3, + Ops.PUSH4: stack_instructions.push4, + Ops.PUSH5: stack_instructions.push5, + Ops.PUSH6: stack_instructions.push6, + Ops.PUSH7: stack_instructions.push7, + Ops.PUSH8: stack_instructions.push8, + Ops.PUSH9: stack_instructions.push9, + Ops.PUSH10: stack_instructions.push10, + Ops.PUSH11: stack_instructions.push11, + Ops.PUSH12: stack_instructions.push12, + Ops.PUSH13: stack_instructions.push13, + Ops.PUSH14: stack_instructions.push14, + Ops.PUSH15: stack_instructions.push15, + Ops.PUSH16: stack_instructions.push16, + Ops.PUSH17: stack_instructions.push17, + Ops.PUSH18: stack_instructions.push18, + Ops.PUSH19: stack_instructions.push19, + Ops.PUSH20: stack_instructions.push20, + Ops.PUSH21: stack_instructions.push21, + Ops.PUSH22: stack_instructions.push22, + Ops.PUSH23: stack_instructions.push23, + Ops.PUSH24: stack_instructions.push24, + Ops.PUSH25: stack_instructions.push25, + Ops.PUSH26: stack_instructions.push26, + Ops.PUSH27: stack_instructions.push27, + Ops.PUSH28: stack_instructions.push28, + Ops.PUSH29: stack_instructions.push29, + Ops.PUSH30: stack_instructions.push30, + Ops.PUSH31: stack_instructions.push31, + Ops.PUSH32: stack_instructions.push32, + Ops.DUP1: stack_instructions.dup1, + Ops.DUP2: stack_instructions.dup2, + Ops.DUP3: stack_instructions.dup3, + Ops.DUP4: stack_instructions.dup4, + Ops.DUP5: stack_instructions.dup5, + Ops.DUP6: stack_instructions.dup6, + Ops.DUP7: stack_instructions.dup7, + Ops.DUP8: stack_instructions.dup8, + Ops.DUP9: stack_instructions.dup9, + Ops.DUP10: stack_instructions.dup10, + Ops.DUP11: stack_instructions.dup11, + Ops.DUP12: stack_instructions.dup12, + Ops.DUP13: stack_instructions.dup13, + Ops.DUP14: stack_instructions.dup14, + Ops.DUP15: stack_instructions.dup15, + Ops.DUP16: stack_instructions.dup16, + Ops.SWAP1: stack_instructions.swap1, + Ops.SWAP2: stack_instructions.swap2, + Ops.SWAP3: stack_instructions.swap3, + Ops.SWAP4: stack_instructions.swap4, + Ops.SWAP5: stack_instructions.swap5, + Ops.SWAP6: stack_instructions.swap6, + Ops.SWAP7: stack_instructions.swap7, + Ops.SWAP8: stack_instructions.swap8, + Ops.SWAP9: stack_instructions.swap9, + Ops.SWAP10: stack_instructions.swap10, + Ops.SWAP11: stack_instructions.swap11, + Ops.SWAP12: stack_instructions.swap12, + Ops.SWAP13: stack_instructions.swap13, + Ops.SWAP14: stack_instructions.swap14, + Ops.SWAP15: stack_instructions.swap15, + Ops.SWAP16: stack_instructions.swap16, +} diff --git a/src/ethereum/vm/instructions.py b/src/ethereum/vm/instructions/arithmetic.py similarity index 54% rename from src/ethereum/vm/instructions.py rename to src/ethereum/vm/instructions/arithmetic.py index dc9afcdf9dc..48a050f8ff6 100644 --- a/src/ethereum/vm/instructions.py +++ b/src/ethereum/vm/instructions/arithmetic.py @@ -1,6 +1,6 @@ """ -Ethereum Virtual Machine (EVM) Instructions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Ethereum Virtual Machine (EVM) Arithmetic Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. contents:: Table of Contents :backlinks: none @@ -9,44 +9,22 @@ Introduction ------------ -Implementations of the instructions understood by the EVM. +Implementations of the EVM Arithmetic instructions. """ - -from functools import partial from typing import cast -from ..base_types import U255_CEIL_VALUE, U256, U256_MAX_VALUE -from ..utils import get_sign -from . import Evm -from .gas import ( +from ...base_types import U255_CEIL_VALUE, U256, U256_MAX_VALUE +from ...utils import get_sign +from .. import Evm +from ..gas import ( GAS_EXPONENTIATION, GAS_LOW, GAS_MID, - GAS_STORAGE_CLEAR_REFUND, - GAS_STORAGE_SET, - GAS_STORAGE_UPDATE, GAS_VERY_LOW, subtract_gas, ) -from .stack import pop, push - - -def stop(evm: Evm) -> None: - """ - Stop further execution of EVM code. - - Parameters - ---------- - evm : - The current EVM frame. - """ - evm.running = False - - -# -# Arithmetic Operations -# +from ..stack import pop, push def add(evm: Evm) -> None: @@ -387,199 +365,3 @@ def signextend(evm: Evm) -> None: ) push(evm.stack, result) - - -def sstore(evm: Evm) -> None: - """ - Stores a value at a certain key in the current context's storage. - - Parameters - ---------- - evm : - The current EVM frame. - - Raises - ------ - StackUnderflowError - If `len(stack)` is less than `2`. - OutOfGasError - If `evm.gas_left` is less than `20000`. - """ - key = pop(evm.stack).to_be_bytes32() - new_value = pop(evm.stack) - current_value = evm.env.state[evm.current].storage.get(key, U256(0)) - - # TODO: SSTORE gas usage hasn't been tested yet. Testing this needs - # other opcodes to be implemented. - # Calculating the gas needed for the storage - if new_value != 0 and current_value == 0: - gas_cost = GAS_STORAGE_SET - else: - gas_cost = GAS_STORAGE_UPDATE - - evm.gas_left = subtract_gas(evm.gas_left, gas_cost) - - # TODO: Refund counter hasn't been tested yet. Testing this needs other - # Opcodes to be implemented - if new_value == 0 and current_value != 0: - evm.refund_counter += GAS_STORAGE_CLEAR_REFUND - - if new_value == 0: - # Deletes a k-v pair from dict if key is present, else does nothing - evm.env.state[evm.current].storage.pop(key, None) - else: - evm.env.state[evm.current].storage[key] = new_value - - -def push_n(evm: Evm, num_bytes: int) -> None: - """ - Pushes a N-byte immediate onto the stack. - - Parameters - ---------- - evm : - The current EVM frame. - - num_bytes : - The number of immediate bytes to be read from the code and pushed to - the stack. - - Raises - ------ - StackOverflowError - If `len(stack)` is equals `1024`. - OutOfGasError - If `evm.gas_left` is less than `GAS_VERY_LOW`. - """ - assert evm.pc + num_bytes < len(evm.code) - evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) - - data_to_push = U256.from_be_bytes( - evm.code[evm.pc + 1 : evm.pc + num_bytes + 1] - ) - push(evm.stack, data_to_push) - - evm.pc += num_bytes - - -def dup_n(evm: Evm, item_number: int) -> None: - """ - Duplicate the Nth stack item (from top of the stack) to the top of stack. - - Parameters - ---------- - evm : - The current EVM frame. - - item_number : - The stack item number (0-indexed from top of stack) to be duplicated - to the top of stack. - - Raises - ------ - OutOfGasError - If `evm.gas_left` is less than `GAS_VERY_LOW`. - """ - assert item_number < len(evm.stack) - evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) - - data_to_duplicate = evm.stack[len(evm.stack) - 1 - item_number] - push(evm.stack, data_to_duplicate) - - -def swap_n(evm: Evm, item_number: int) -> None: - """ - Swap the 1st and Nth items in the stack. All items are 0-indexed from the - top of the stack. - - Parameters - ---------- - evm : - The current EVM frame. - - item_number : - The stack item number (0-indexed from top of stack) to be swapped - with the top of stack element. - - Raises - ------ - OutOfGasError - If `evm.gas_left` is less than `GAS_VERY_LOW`. - """ - assert item_number < len(evm.stack) - evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) - - top_element_idx = len(evm.stack) - 1 - nth_element_idx = len(evm.stack) - 1 - item_number - evm.stack[top_element_idx], evm.stack[nth_element_idx] = ( - evm.stack[nth_element_idx], - evm.stack[top_element_idx], - ) - - -push1 = partial(push_n, num_bytes=1) -push2 = partial(push_n, num_bytes=2) -push3 = partial(push_n, num_bytes=3) -push4 = partial(push_n, num_bytes=4) -push5 = partial(push_n, num_bytes=5) -push6 = partial(push_n, num_bytes=6) -push7 = partial(push_n, num_bytes=7) -push8 = partial(push_n, num_bytes=8) -push9 = partial(push_n, num_bytes=9) -push10 = partial(push_n, num_bytes=10) -push11 = partial(push_n, num_bytes=11) -push12 = partial(push_n, num_bytes=12) -push13 = partial(push_n, num_bytes=13) -push14 = partial(push_n, num_bytes=14) -push15 = partial(push_n, num_bytes=15) -push16 = partial(push_n, num_bytes=16) -push17 = partial(push_n, num_bytes=17) -push18 = partial(push_n, num_bytes=18) -push19 = partial(push_n, num_bytes=19) -push20 = partial(push_n, num_bytes=20) -push21 = partial(push_n, num_bytes=21) -push22 = partial(push_n, num_bytes=22) -push23 = partial(push_n, num_bytes=23) -push24 = partial(push_n, num_bytes=24) -push25 = partial(push_n, num_bytes=25) -push26 = partial(push_n, num_bytes=26) -push27 = partial(push_n, num_bytes=27) -push28 = partial(push_n, num_bytes=28) -push29 = partial(push_n, num_bytes=29) -push30 = partial(push_n, num_bytes=30) -push31 = partial(push_n, num_bytes=31) -push32 = partial(push_n, num_bytes=32) - -dup1 = partial(dup_n, item_number=0) -dup2 = partial(dup_n, item_number=1) -dup3 = partial(dup_n, item_number=2) -dup4 = partial(dup_n, item_number=3) -dup5 = partial(dup_n, item_number=4) -dup6 = partial(dup_n, item_number=5) -dup7 = partial(dup_n, item_number=6) -dup8 = partial(dup_n, item_number=7) -dup9 = partial(dup_n, item_number=8) -dup10 = partial(dup_n, item_number=9) -dup11 = partial(dup_n, item_number=10) -dup12 = partial(dup_n, item_number=11) -dup13 = partial(dup_n, item_number=12) -dup14 = partial(dup_n, item_number=13) -dup15 = partial(dup_n, item_number=14) -dup16 = partial(dup_n, item_number=15) - -swap1 = partial(swap_n, item_number=1) -swap2 = partial(swap_n, item_number=2) -swap3 = partial(swap_n, item_number=3) -swap4 = partial(swap_n, item_number=4) -swap5 = partial(swap_n, item_number=5) -swap6 = partial(swap_n, item_number=6) -swap7 = partial(swap_n, item_number=7) -swap8 = partial(swap_n, item_number=8) -swap9 = partial(swap_n, item_number=9) -swap10 = partial(swap_n, item_number=10) -swap11 = partial(swap_n, item_number=11) -swap12 = partial(swap_n, item_number=12) -swap13 = partial(swap_n, item_number=13) -swap14 = partial(swap_n, item_number=14) -swap15 = partial(swap_n, item_number=15) -swap16 = partial(swap_n, item_number=16) diff --git a/src/ethereum/vm/instructions/computation.py b/src/ethereum/vm/instructions/computation.py new file mode 100644 index 00000000000..67a36f143f6 --- /dev/null +++ b/src/ethereum/vm/instructions/computation.py @@ -0,0 +1,27 @@ +""" +Ethereum Virtual Machine (EVM) Computation Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Arithmetic instructions. +""" + +from .. import Evm + + +def stop(evm: Evm) -> None: + """ + Stop further execution of EVM code. + + Parameters + ---------- + evm : + The current EVM frame. + """ + evm.running = False diff --git a/src/ethereum/vm/instructions/stack.py b/src/ethereum/vm/instructions/stack.py new file mode 100644 index 00000000000..b4cee9cf531 --- /dev/null +++ b/src/ethereum/vm/instructions/stack.py @@ -0,0 +1,174 @@ +""" +Ethereum Virtual Machine (EVM) stack Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM stack related instructions. +""" + +from functools import partial + +from ...base_types import U256 +from .. import Evm +from ..gas import GAS_VERY_LOW, subtract_gas +from ..stack import push + + +def push_n(evm: Evm, num_bytes: int) -> None: + """ + Pushes a N-byte immediate onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + num_bytes : + The number of immediate bytes to be read from the code and pushed to + the stack. + + Raises + ------ + StackOverflowError + If `len(stack)` is equals `1024`. + OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + assert evm.pc + num_bytes < len(evm.code) + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + data_to_push = U256.from_be_bytes( + evm.code[evm.pc + 1 : evm.pc + num_bytes + 1] + ) + push(evm.stack, data_to_push) + + evm.pc += num_bytes + + +def dup_n(evm: Evm, item_number: int) -> None: + """ + Duplicate the Nth stack item (from top of the stack) to the top of stack. + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be duplicated + to the top of stack. + + Raises + ------ + OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + assert item_number < len(evm.stack) + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + data_to_duplicate = evm.stack[len(evm.stack) - 1 - item_number] + push(evm.stack, data_to_duplicate) + + +def swap_n(evm: Evm, item_number: int) -> None: + """ + Swap the 1st and Nth items in the stack. All items are 0-indexed from the + top of the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be swapped + with the top of stack element. + + Raises + ------ + OutOfGasError + If `evm.gas_left` is less than `GAS_VERY_LOW`. + """ + assert item_number < len(evm.stack) + evm.gas_left = subtract_gas(evm.gas_left, GAS_VERY_LOW) + + top_element_idx = len(evm.stack) - 1 + nth_element_idx = len(evm.stack) - 1 - item_number + evm.stack[top_element_idx], evm.stack[nth_element_idx] = ( + evm.stack[nth_element_idx], + evm.stack[top_element_idx], + ) + + +push1 = partial(push_n, num_bytes=1) +push2 = partial(push_n, num_bytes=2) +push3 = partial(push_n, num_bytes=3) +push4 = partial(push_n, num_bytes=4) +push5 = partial(push_n, num_bytes=5) +push6 = partial(push_n, num_bytes=6) +push7 = partial(push_n, num_bytes=7) +push8 = partial(push_n, num_bytes=8) +push9 = partial(push_n, num_bytes=9) +push10 = partial(push_n, num_bytes=10) +push11 = partial(push_n, num_bytes=11) +push12 = partial(push_n, num_bytes=12) +push13 = partial(push_n, num_bytes=13) +push14 = partial(push_n, num_bytes=14) +push15 = partial(push_n, num_bytes=15) +push16 = partial(push_n, num_bytes=16) +push17 = partial(push_n, num_bytes=17) +push18 = partial(push_n, num_bytes=18) +push19 = partial(push_n, num_bytes=19) +push20 = partial(push_n, num_bytes=20) +push21 = partial(push_n, num_bytes=21) +push22 = partial(push_n, num_bytes=22) +push23 = partial(push_n, num_bytes=23) +push24 = partial(push_n, num_bytes=24) +push25 = partial(push_n, num_bytes=25) +push26 = partial(push_n, num_bytes=26) +push27 = partial(push_n, num_bytes=27) +push28 = partial(push_n, num_bytes=28) +push29 = partial(push_n, num_bytes=29) +push30 = partial(push_n, num_bytes=30) +push31 = partial(push_n, num_bytes=31) +push32 = partial(push_n, num_bytes=32) + +dup1 = partial(dup_n, item_number=0) +dup2 = partial(dup_n, item_number=1) +dup3 = partial(dup_n, item_number=2) +dup4 = partial(dup_n, item_number=3) +dup5 = partial(dup_n, item_number=4) +dup6 = partial(dup_n, item_number=5) +dup7 = partial(dup_n, item_number=6) +dup8 = partial(dup_n, item_number=7) +dup9 = partial(dup_n, item_number=8) +dup10 = partial(dup_n, item_number=9) +dup11 = partial(dup_n, item_number=10) +dup12 = partial(dup_n, item_number=11) +dup13 = partial(dup_n, item_number=12) +dup14 = partial(dup_n, item_number=13) +dup15 = partial(dup_n, item_number=14) +dup16 = partial(dup_n, item_number=15) + +swap1 = partial(swap_n, item_number=1) +swap2 = partial(swap_n, item_number=2) +swap3 = partial(swap_n, item_number=3) +swap4 = partial(swap_n, item_number=4) +swap5 = partial(swap_n, item_number=5) +swap6 = partial(swap_n, item_number=6) +swap7 = partial(swap_n, item_number=7) +swap8 = partial(swap_n, item_number=8) +swap9 = partial(swap_n, item_number=9) +swap10 = partial(swap_n, item_number=10) +swap11 = partial(swap_n, item_number=11) +swap12 = partial(swap_n, item_number=12) +swap13 = partial(swap_n, item_number=13) +swap14 = partial(swap_n, item_number=14) +swap15 = partial(swap_n, item_number=15) +swap16 = partial(swap_n, item_number=16) diff --git a/src/ethereum/vm/instructions/storage.py b/src/ethereum/vm/instructions/storage.py new file mode 100644 index 00000000000..d1e4aec498e --- /dev/null +++ b/src/ethereum/vm/instructions/storage.py @@ -0,0 +1,65 @@ +""" +Ethereum Virtual Machine (EVM) Storage Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM storage related instructions. +""" + +from ...base_types import U256 +from .. import Evm +from ..gas import ( + GAS_STORAGE_CLEAR_REFUND, + GAS_STORAGE_SET, + GAS_STORAGE_UPDATE, + subtract_gas, +) +from ..stack import pop + + +def sstore(evm: Evm) -> None: + """ + Stores a value at a certain key in the current context's storage. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + StackUnderflowError + If `len(stack)` is less than `2`. + OutOfGasError + If `evm.gas_left` is less than `20000`. + """ + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + current_value = evm.env.state[evm.current].storage.get(key, U256(0)) + + # TODO: SSTORE gas usage hasn't been tested yet. Testing this needs + # other opcodes to be implemented. + # Calculating the gas needed for the storage + if new_value != 0 and current_value == 0: + gas_cost = GAS_STORAGE_SET + else: + gas_cost = GAS_STORAGE_UPDATE + + evm.gas_left = subtract_gas(evm.gas_left, gas_cost) + + # TODO: Refund counter hasn't been tested yet. Testing this needs other + # Opcodes to be implemented + if new_value == 0 and current_value != 0: + evm.refund_counter += GAS_STORAGE_CLEAR_REFUND + + if new_value == 0: + # Deletes a k-v pair from dict if key is present, else does nothing + evm.env.state[evm.current].storage.pop(key, None) + else: + evm.env.state[evm.current].storage[key] = new_value diff --git a/src/ethereum/vm/interpreter.py b/src/ethereum/vm/interpreter.py index 0f13de08e8c..862d54862ed 100644 --- a/src/ethereum/vm/interpreter.py +++ b/src/ethereum/vm/interpreter.py @@ -17,7 +17,7 @@ from ..base_types import U256, Uint from ..eth_types import Address, Log from . import Environment, Evm -from .ops import op_implementation +from .instructions import Ops, op_implementation def process_call( @@ -85,7 +85,7 @@ def process_call( evm.env.state[evm.current].balance += evm.value while evm.running: - op = evm.code[evm.pc] + op = Ops(evm.code[evm.pc]) op_implementation[op](evm) evm.pc += 1 diff --git a/src/ethereum/vm/ops.py b/src/ethereum/vm/ops.py deleted file mode 100644 index d27e1c2f4a1..00000000000 --- a/src/ethereum/vm/ops.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -Instruction Encoding (Opcodes) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. contents:: Table of Contents - :backlinks: none - :local: - -Introduction ------------- - -Machine readable representations of EVM instructions, and a mapping to their -implementations. -""" - -from typing import Callable, Dict - -from . import instructions - -# Arithmetic Operations -STOP = 0x00 -ADD = 0x01 -MUL = 0x02 -SUB = 0x03 -DIV = 0x04 -SDIV = 0x05 -MOD = 0x06 -SMOD = 0x07 -ADDMOD = 0x08 -MULMOD = 0x09 -EXP = 0x0A -SIGNEXTEND = 0x0B - -# Push Operations -PUSH1 = 0x60 -PUSH2 = 0x61 -PUSH3 = 0x62 -PUSH4 = 0x63 -PUSH5 = 0x64 -PUSH6 = 0x65 -PUSH7 = 0x66 -PUSH8 = 0x67 -PUSH9 = 0x68 -PUSH10 = 0x69 -PUSH11 = 0x6A -PUSH12 = 0x6B -PUSH13 = 0x6C -PUSH14 = 0x6D -PUSH15 = 0x6E -PUSH16 = 0x6F -PUSH17 = 0x70 -PUSH18 = 0x71 -PUSH19 = 0x72 -PUSH20 = 0x73 -PUSH21 = 0x74 -PUSH22 = 0x75 -PUSH23 = 0x76 -PUSH24 = 0x77 -PUSH25 = 0x78 -PUSH26 = 0x79 -PUSH27 = 0x7A -PUSH28 = 0x7B -PUSH29 = 0x7C -PUSH30 = 0x7D -PUSH31 = 0x7E -PUSH32 = 0x7F - -# Dup operations -DUP1 = 0x80 -DUP2 = 0x81 -DUP3 = 0x82 -DUP4 = 0x83 -DUP5 = 0x84 -DUP6 = 0x85 -DUP7 = 0x86 -DUP8 = 0x87 -DUP9 = 0x88 -DUP10 = 0x89 -DUP11 = 0x8A -DUP12 = 0x8B -DUP13 = 0x8C -DUP14 = 0x8D -DUP15 = 0x8E -DUP16 = 0x8F - -# Swap operations -SWAP1 = 0x90 -SWAP2 = 0x91 -SWAP3 = 0x92 -SWAP4 = 0x93 -SWAP5 = 0x94 -SWAP6 = 0x95 -SWAP7 = 0x96 -SWAP8 = 0x97 -SWAP9 = 0x98 -SWAP10 = 0x99 -SWAP11 = 0x9A -SWAP12 = 0x9B -SWAP13 = 0x9C -SWAP14 = 0x9D -SWAP15 = 0x9E -SWAP16 = 0x9F - -SSTORE = 0x55 - - -op_implementation: Dict[int, Callable] = { - STOP: instructions.stop, - ADD: instructions.add, - MUL: instructions.mul, - SUB: instructions.sub, - DIV: instructions.div, - SDIV: instructions.sdiv, - MOD: instructions.mod, - SMOD: instructions.smod, - ADDMOD: instructions.addmod, - MULMOD: instructions.mulmod, - EXP: instructions.exp, - SIGNEXTEND: instructions.signextend, - SSTORE: instructions.sstore, - PUSH1: instructions.push1, - PUSH2: instructions.push2, - PUSH3: instructions.push3, - PUSH4: instructions.push4, - PUSH5: instructions.push5, - PUSH6: instructions.push6, - PUSH7: instructions.push7, - PUSH8: instructions.push8, - PUSH9: instructions.push9, - PUSH10: instructions.push10, - PUSH11: instructions.push11, - PUSH12: instructions.push12, - PUSH13: instructions.push13, - PUSH14: instructions.push14, - PUSH15: instructions.push15, - PUSH16: instructions.push16, - PUSH17: instructions.push17, - PUSH18: instructions.push18, - PUSH19: instructions.push19, - PUSH20: instructions.push20, - PUSH21: instructions.push21, - PUSH22: instructions.push22, - PUSH23: instructions.push23, - PUSH24: instructions.push24, - PUSH25: instructions.push25, - PUSH26: instructions.push26, - PUSH27: instructions.push27, - PUSH28: instructions.push28, - PUSH29: instructions.push29, - PUSH30: instructions.push30, - PUSH31: instructions.push31, - PUSH32: instructions.push32, - DUP1: instructions.dup1, - DUP2: instructions.dup2, - DUP3: instructions.dup3, - DUP4: instructions.dup4, - DUP5: instructions.dup5, - DUP6: instructions.dup6, - DUP7: instructions.dup7, - DUP8: instructions.dup8, - DUP9: instructions.dup9, - DUP10: instructions.dup10, - DUP11: instructions.dup11, - DUP12: instructions.dup12, - DUP13: instructions.dup13, - DUP14: instructions.dup14, - DUP15: instructions.dup15, - DUP16: instructions.dup16, - SWAP1: instructions.swap1, - SWAP2: instructions.swap2, - SWAP3: instructions.swap3, - SWAP4: instructions.swap4, - SWAP5: instructions.swap5, - SWAP6: instructions.swap6, - SWAP7: instructions.swap7, - SWAP8: instructions.swap8, - SWAP9: instructions.swap9, - SWAP10: instructions.swap10, - SWAP11: instructions.swap11, - SWAP12: instructions.swap12, - SWAP13: instructions.swap13, - SWAP14: instructions.swap14, - SWAP15: instructions.swap15, - SWAP16: instructions.swap16, -}