From d58f6a7690b0ca53a07369e005b4802d921cdf3e Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Thu, 16 Nov 2017 09:23:49 -0700 Subject: [PATCH 1/3] Byzantium fork rules --- .travis.yml | 3 +- evm/chains/mainnet/__init__.py | 2 + evm/constants.py | 16 +++ evm/exceptions.py | 27 +++- evm/logic/call.py | 54 +++++++- evm/logic/context.py | 38 ++++++ evm/logic/system.py | 21 ++- evm/mnemonics.py | 4 + evm/opcode_values.py | 4 + evm/precompiles/__init__.py | 4 + evm/precompiles/ecadd.py | 54 ++++++++ evm/precompiles/ecmul.py | 51 +++++++ evm/precompiles/ecpairing.py | 107 +++++++++++++++ evm/precompiles/modexp.py | 126 ++++++++++++++++++ evm/utils/bn128.py | 25 ++++ evm/utils/numeric.py | 9 ++ evm/vm/computation.py | 45 +++++-- evm/vm/forks/__init__.py | 3 + evm/vm/forks/byzantium/__init__.py | 59 ++++++++ evm/vm/forks/byzantium/blocks.py | 34 +++++ evm/vm/forks/byzantium/headers.py | 78 +++++++++++ evm/vm/forks/byzantium/opcodes.py | 126 ++++++++++++++++++ evm/vm/forks/byzantium/transactions.py | 30 +++++ evm/vm/forks/frontier/__init__.py | 56 ++++---- evm/vm/message.py | 7 +- setup.py | 1 + .../test_get_highest_bit_length.py | 20 +++ tests/core/vm/test_computation.py | 47 +++++++ tests/core/vm/test_modexp_precompile.py | 63 +++++++++ tests/json-fixtures/test_blockchain.py | 10 +- tests/json-fixtures/test_state.py | 7 +- 31 files changed, 1076 insertions(+), 55 deletions(-) create mode 100644 evm/precompiles/ecadd.py create mode 100644 evm/precompiles/ecmul.py create mode 100644 evm/precompiles/ecpairing.py create mode 100644 evm/precompiles/modexp.py create mode 100644 evm/utils/bn128.py create mode 100644 evm/vm/forks/byzantium/__init__.py create mode 100644 evm/vm/forks/byzantium/blocks.py create mode 100644 evm/vm/forks/byzantium/headers.py create mode 100644 evm/vm/forks/byzantium/opcodes.py create mode 100644 evm/vm/forks/byzantium/transactions.py create mode 100644 tests/core/numeric-utils/test_get_highest_bit_length.py create mode 100644 tests/core/vm/test_modexp_precompile.py diff --git a/.travis.yml b/.travis.yml index 0e74e8a749..d35a722d60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,11 +7,12 @@ env: global: - PYTEST_ADDOPTS="-n 2" matrix: + # byzantium goes first because it takes the longest. + - TOX_POSARGS="-e py35-state-byzantium" - TOX_POSARGS="-e py35-state-frontier" - TOX_POSARGS="-e py35-state-homestead" - TOX_POSARGS="-e py35-state-eip150" - TOX_POSARGS="-e py35-state-eip158" - #- TOX_POSARGS="-e py35-state-byzantium" #- TOX_POSARGS="-e py35-state-constantinople" #- TOX_POSARGS="-e py35-state-metropolis" - TOX_POSARGS="-e py35-blockchain" diff --git a/evm/chains/mainnet/__init__.py b/evm/chains/mainnet/__init__.py index 775a8975eb..324c5ef826 100644 --- a/evm/chains/mainnet/__init__.py +++ b/evm/chains/mainnet/__init__.py @@ -9,6 +9,7 @@ FrontierVM, HomesteadVM, SpuriousDragonVM, + ByzantiumVM, ) @@ -17,6 +18,7 @@ (constants.HOMESTEAD_MAINNET_BLOCK, HomesteadVM), (constants.EIP150_MAINNET_BLOCK, EIP150VM), (constants.SPURIOUS_DRAGON_MAINNET_BLOCK, SpuriousDragonVM), + (constants.BYZANTIUM_MAINNET_BLOCK, ByzantiumVM), ) diff --git a/evm/constants.py b/evm/constants.py index bd3e97797d..5e859fe3d2 100644 --- a/evm/constants.py +++ b/evm/constants.py @@ -90,6 +90,12 @@ GAS_ECRECOVER = 3000 +GAS_ECADD = 500 +GAS_ECMUL = 40000 + +GAS_ECPAIRING_BASE = 100000 +GAS_ECPAIRING_PER_POINT = 80000 + # # Gas Limit @@ -111,6 +117,7 @@ FRONTIER_DIFFICULTY_ADJUSTMENT_CUTOFF = 13 HOMESTEAD_DIFFICULTY_ADJUSTMENT_CUTOFF = 10 +BYZANTIUM_DIFFICULTY_ADJUSTMENT_CUTOFF = 9 BOMB_EXPONENTIAL_PERIOD = 100000 BOMB_EXPONENTIAL_FREE_PERIODS = 2 @@ -119,6 +126,7 @@ # Mining # BLOCK_REWARD = 5 * denoms.ether +EIP649_BLOCK_REWARD = 3 * denoms.ether UNCLE_DEPTH_PENALTY_FACTOR = 8 MAX_UNCLE_DEPTH = 6 @@ -193,3 +201,11 @@ # https://github.com/ethereum/EIPs/issues/170 EIP170_CODE_SIZE_LIMIT = 24577 + +# +# ByzantiumVM +# +BYZANTIUM_MAINNET_BLOCK = 4370000 +BYZANTIUM_ROPSTEN_BLOCK = 1700000 + +GAS_MOD_EXP_QUADRATIC_DENOMINATOR = 20 diff --git a/evm/exceptions.py b/evm/exceptions.py index 665ae33004..3a83eb72b5 100644 --- a/evm/exceptions.py +++ b/evm/exceptions.py @@ -51,7 +51,8 @@ class VMError(PyEVMError): """ Class of errors which can be raised during VM execution. """ - pass + burns_gas = True + zeros_return_data = True class OutOfGas(VMError): @@ -109,3 +110,27 @@ class ContractCreationCollision(VMError): Error signaling that there was an address collision during contract creation. """ pass + + +class Revert(VMError): + """ + Error used by the REVERT opcode + """ + burns_gas = False + zeros_return_data = False + + +class WriteProtection(VMError): + """ + Error raised if an attempt to modify the state database is made while + operating inside of a STATICCALL context. + """ + pass + + +class OutOfBoundsRead(VMError): + """ + Error raised to indicate an attempt was made to read data beyond then + boundaries of the buffer (such as with RETURNDATACOPY) + """ + pass diff --git a/evm/logic/call.py b/evm/logic/call.py index abda3107ab..7a393c8b78 100644 --- a/evm/logic/call.py +++ b/evm/logic/call.py @@ -2,6 +2,7 @@ from evm.exceptions import ( OutOfGas, + WriteProtection, ) from evm.opcode import ( Opcode, @@ -42,6 +43,7 @@ def __call__(self, computation): memory_output_start_position, memory_output_size, should_transfer_value, + is_static, ) = self.get_call_params(computation) computation.extend_memory(memory_input_start_position, memory_input_size) @@ -62,6 +64,7 @@ def __call__(self, computation): stack_too_deep = computation.msg.depth + 1 > constants.STACK_DEPTH_LIMIT if insufficient_funds or stack_too_deep: + computation.return_data = b'' if insufficient_funds: err_message = "Insufficient Funds: have: {0} | need: {1}".format( sender_balance, @@ -94,6 +97,7 @@ def __call__(self, computation): 'code': code, 'code_address': code_address, 'should_transfer_value': should_transfer_value, + 'is_static': is_static, } if sender is not None: child_msg_kwargs['sender'] = sender @@ -105,14 +109,18 @@ def __call__(self, computation): if child_computation.error: computation.stack.push(0) else: + computation.stack.push(1) + + if not child_computation.error or not child_computation.error.zeros_return_data: actual_output_size = min(memory_output_size, len(child_computation.output)) - computation.gas_meter.return_gas(child_computation.gas_meter.gas_remaining) computation.memory.write( memory_output_start_position, actual_output_size, child_computation.output[:actual_output_size], ) - computation.stack.push(1) + + if not child_computation.error or not child_computation.error.burns_gas: + computation.gas_meter.return_gas(child_computation.gas_meter.gas_remaining) class Call(BaseCall): @@ -146,6 +154,7 @@ def get_call_params(self, computation): memory_output_start_position, memory_output_size, True, # should_transfer_value, + computation.msg.is_static, # is_static ) @@ -179,6 +188,7 @@ def get_call_params(self, computation): memory_output_start_position, memory_output_size, True, # should_transfer_value, + computation.msg.is_static, # is_static ) @@ -215,6 +225,7 @@ def get_call_params(self, computation): memory_output_start_position, memory_output_size, False, # should_transfer_value, + computation.msg.is_static, # is_static ) @@ -279,3 +290,42 @@ def compute_msg_extra_gas(self, computation, gas, to, value): transfer_gas_fee = constants.GAS_CALLVALUE if value else 0 create_gas_fee = constants.GAS_NEWACCOUNT if (account_is_dead and value) else 0 return transfer_gas_fee + create_gas_fee + + +# +# Byzantium +# +class StaticCall(CallEIP161): + def get_call_params(self, computation): + gas = computation.stack.pop(type_hint=constants.UINT256) + to = force_bytes_to_address(computation.stack.pop(type_hint=constants.BYTES)) + + ( + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + ) = computation.stack.pop(num_items=4, type_hint=constants.UINT256) + + return ( + gas, + 0, # value + to, + None, # sender + None, # code_address + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + False, # should_transfer_value, + True, # is_static + ) + + +class CallByzantium(CallEIP161): + def get_call_params(self, computation): + call_params = super(CallByzantium, self).get_call_params(computation) + value = call_params[1] + if computation.msg.is_static and value != 0: + raise WriteProtection("Cannot modify state while inside of a STATICCALL context") + return call_params diff --git a/evm/logic/context.py b/evm/logic/context.py index b933534755..a4e2b19bb5 100644 --- a/evm/logic/context.py +++ b/evm/logic/context.py @@ -1,4 +1,7 @@ from evm import constants +from evm.exceptions import ( + OutOfBoundsRead, +) from evm.utils.address import ( force_bytes_to_address, @@ -138,3 +141,38 @@ def extcodecopy(computation): padded_code_bytes = pad_right(code_bytes, size, b'\x00') computation.memory.write(mem_start_position, size, padded_code_bytes) + + +def returndatasize(computation): + size = len(computation.return_data) + computation.stack.push(size) + + +def returndatacopy(computation): + ( + mem_start_position, + returndata_start_position, + size, + ) = computation.stack.pop(num_items=3, type_hint=constants.UINT256) + + if returndata_start_position + size > len(computation.return_data): + raise OutOfBoundsRead( + "Return data length is not sufficient to satisfy request. Asked " + "for data from index {0} to {1}. Return data is {2} bytes in " + "length.".format( + returndata_start_position, + returndata_start_position + size, + len(computation.return_data), + ) + ) + + computation.extend_memory(mem_start_position, size) + + word_count = ceil32(size) // 32 + copy_gas_cost = word_count * constants.GAS_COPY + + computation.gas_meter.consume_gas(copy_gas_cost, reason="RETURNDATACOPY fee") + + value = computation.return_data[returndata_start_position: returndata_start_position + size] + + computation.memory.write(mem_start_position, size, value) diff --git a/evm/logic/system.py b/evm/logic/system.py index ce39a4f3c9..6cc50f3fd3 100644 --- a/evm/logic/system.py +++ b/evm/logic/system.py @@ -2,6 +2,8 @@ from evm import mnemonics from evm.exceptions import ( Halt, + Revert, + WriteProtection, ) from evm.opcode import ( @@ -28,6 +30,16 @@ def return_op(computation): raise Halt('RETURN') +def revert(computation): + start_position, size = computation.stack.pop(num_items=2, type_hint=constants.UINT256) + + computation.extend_memory(start_position, size) + + output = computation.memory.read(start_position, size) + computation.output = bytes(output) + raise Revert(bytes(output)) + + def selfdestruct(computation): beneficiary = force_bytes_to_address(computation.stack.pop(type_hint=constants.BYTES)) _selfdestruct(computation, beneficiary) @@ -147,10 +159,17 @@ def __call__(self, computation): if child_computation.error: computation.stack.push(0) else: - computation.gas_meter.return_gas(child_computation.gas_meter.gas_remaining) computation.stack.push(contract_address) + computation.gas_meter.return_gas(child_computation.gas_meter.gas_remaining) class CreateEIP150(Create): def max_child_gas_modifier(self, gas): return max_child_gas_eip150(gas) + + +class CreateByzantium(CreateEIP150): + def __call__(self, computation): + if computation.msg.is_static: + raise WriteProtection("Cannot modify state while inside of a STATICCALL context") + return super(CreateEIP150, self).__call__(computation) diff --git a/evm/mnemonics.py b/evm/mnemonics.py index aa2a8c423a..40e498788d 100644 --- a/evm/mnemonics.py +++ b/evm/mnemonics.py @@ -47,6 +47,8 @@ GASPRICE = 'GASPRICE' EXTCODESIZE = 'EXTCODESIZE' EXTCODECOPY = 'EXTCODECOPY' +RETURNDATASIZE = 'RETURNDATASIZE' +RETURNDATACOPY = 'RETURNDATACOPY' # # Block Information # @@ -71,6 +73,7 @@ MSIZE = 'MSIZE' GAS = 'GAS' JUMPDEST = 'JUMPDEST' +REVERT = 'REVERT' # # Push Operations # @@ -158,6 +161,7 @@ CREATE = 'CREATE' CALL = 'CALL' CALLCODE = 'CALLCODE' +STATICCALL = 'STATICCALL' RETURN = 'RETURN' DELEGATECALL = 'DELEGATECALL' SELFDESTRUCT = 'SELFDESTRUCT' diff --git a/evm/opcode_values.py b/evm/opcode_values.py index f17c1b2402..1fb198106b 100644 --- a/evm/opcode_values.py +++ b/evm/opcode_values.py @@ -53,6 +53,8 @@ GASPRICE = 0x3a EXTCODESIZE = 0x3b EXTCODECOPY = 0x3c +RETURNDATASIZE = 0x3d +RETURNDATACOPY = 0x3e # @@ -181,4 +183,6 @@ CALLCODE = 0xf2 RETURN = 0xf3 DELEGATECALL = 0xf4 +STATICCALL = 0xfa +REVERT = 0xfd SELFDESTRUCT = 0xff diff --git a/evm/precompiles/__init__.py b/evm/precompiles/__init__.py index fb1b16a93b..34a514afd7 100644 --- a/evm/precompiles/__init__.py +++ b/evm/precompiles/__init__.py @@ -2,3 +2,7 @@ from .identity import identity # noqa: F401 from .ecrecover import ecrecover # noqa: F401 from .ripemd160 import ripemd160 # noqa: F401 +from .modexp import modexp # noqa: F401 +from .ecadd import ecadd # noqa: F401 +from .ecmul import ecmul # noqa: F401 +from .ecpairing import ecpairing # noqa: F401 diff --git a/evm/precompiles/ecadd.py b/evm/precompiles/ecadd.py new file mode 100644 index 0000000000..cbb7b2fb87 --- /dev/null +++ b/evm/precompiles/ecadd.py @@ -0,0 +1,54 @@ +from py_ecc import ( + optimized_bn128 as bn128, +) + +from evm import constants +from evm.exceptions import ( + ValidationError, + VMError, +) +from evm.utils.bn128 import ( + validate_point, +) +from evm.utils.numeric import ( + big_endian_to_int, + int_to_big_endian, +) +from evm.utils.padding import ( + pad32, + pad32r, +) + + +def ecadd(computation): + computation.gas_meter.consume_gas(constants.GAS_ECADD, reason='ECADD Precompile') + + try: + result = _ecadd(computation.msg.data) + except ValidationError: + raise VMError("Invalid ECADD parameters") + + result_bytes = b''.join(( + pad32(int_to_big_endian(result[0].n)), + pad32(int_to_big_endian(result[1].n)), + )) + computation.output = result_bytes + return computation + + +def _ecadd(data): + x1_bytes = pad32r(data[:32]) + y1_bytes = pad32r(data[32:64]) + x2_bytes = pad32r(data[64:96]) + y2_bytes = pad32r(data[96:128]) + + x1 = big_endian_to_int(x1_bytes) + y1 = big_endian_to_int(y1_bytes) + x2 = big_endian_to_int(x2_bytes) + y2 = big_endian_to_int(y2_bytes) + + p1 = validate_point(x1, y1) + p2 = validate_point(x2, y2) + + result = bn128.normalize(bn128.add(p1, p2)) + return result diff --git a/evm/precompiles/ecmul.py b/evm/precompiles/ecmul.py new file mode 100644 index 0000000000..fd02f9dc04 --- /dev/null +++ b/evm/precompiles/ecmul.py @@ -0,0 +1,51 @@ +from py_ecc import ( + optimized_bn128 as bn128, +) + +from evm import constants +from evm.exceptions import ( + ValidationError, + VMError, +) +from evm.utils.bn128 import ( + validate_point, +) +from evm.utils.numeric import ( + big_endian_to_int, + int_to_big_endian, +) +from evm.utils.padding import ( + pad32, + pad32r, +) + + +def ecmul(computation): + computation.gas_meter.consume_gas(constants.GAS_ECMUL, reason='ECMUL Precompile') + + try: + result = _ecmull(computation.msg.data) + except ValidationError: + raise VMError("Invalid ECMUL parameters") + + result_bytes = b''.join(( + pad32(int_to_big_endian(result[0].n)), + pad32(int_to_big_endian(result[1].n)), + )) + computation.output = result_bytes + return computation + + +def _ecmull(data): + x_bytes = pad32r(data[:32]) + y_bytes = pad32r(data[32:64]) + m_bytes = pad32r(data[64:96]) + + x = big_endian_to_int(x_bytes) + y = big_endian_to_int(y_bytes) + m = big_endian_to_int(m_bytes) + + p = validate_point(x, y) + + result = bn128.normalize(bn128.multiply(p, m)) + return result diff --git a/evm/precompiles/ecpairing.py b/evm/precompiles/ecpairing.py new file mode 100644 index 0000000000..355366bcca --- /dev/null +++ b/evm/precompiles/ecpairing.py @@ -0,0 +1,107 @@ +from cytoolz import ( + curry, + pipe, +) +from py_ecc import ( + optimized_bn128 as bn128, +) + +from evm import constants +from evm.exceptions import ( + ValidationError, + VMError, +) +from evm.utils.bn128 import ( + validate_point, +) +from evm.utils.numeric import ( + big_endian_to_int, +) +from evm.utils.padding import ( + pad32, +) + + +ZERO = (bn128.FQ2.one(), bn128.FQ2.one(), bn128.FQ2.zero()) +EXPONENT = bn128.FQ12.one() + + +def ecpairing(computation): + if len(computation.msg.data) % 192: + # data length must be an exact multiple of 192 + raise VMError("Invalid ECPAIRING parameters") + + num_points = len(computation.msg.data) // 192 + gas_fee = constants.GAS_ECPAIRING_BASE + num_points * constants.GAS_ECPAIRING_PER_POINT + + computation.gas_meter.consume_gas(gas_fee, reason='ECPAIRING Precompile') + + try: + result = _ecpairing(computation.msg.data) + except ValidationError: + raise VMError("Invalid ECPAIRING parameters") + + if result is True: + computation.output = pad32(b'\x01') + elif result is False: + computation.output = pad32(b'\x00') + else: + raise Exception("Invariant: unreachable code path") + return computation + + +def _ecpairing(data): + exponent = bn128.FQ12.one() + + processing_pipeline = ( + _process_point(data[start_idx:start_idx + 192]) + for start_idx + in range(0, len(data), 192) + ) + exponent = pipe(bn128.FQ12.one(), *processing_pipeline) + + result = bn128.final_exponentiate(exponent) == bn128.FQ12.one() + return result + + +@curry +def _process_point(data_buffer, exponent): + x1, y1, x2_i, x2_r, y2_i, y2_r = _extract_point(data_buffer) + p1 = validate_point(x1, y1) + + for v in (x2_i, x2_r, y2_i, y2_r): + if v >= bn128.field_modulus: + raise ValidationError("value greater than field modulus") + + fq2_x = bn128.FQ2([x2_r, x2_i]) + fq2_y = bn128.FQ2([y2_r, y2_i]) + + if (fq2_x, fq2_y) != (bn128.FQ2.zero(), bn128.FQ2.zero()): + p2 = (fq2_x, fq2_y, bn128.FQ2.one()) + if not bn128.is_on_curve(p2, bn128.b2): + raise ValidationError("point is not on curve") + else: + p2 = ZERO + + if bn128.multiply(p2, bn128.curve_order)[-1] != bn128.FQ2.zero(): + raise ValidationError("TODO: what case is this?????") + + return exponent * bn128.pairing(p2, p1, final_exponentiate=False) + + +def _extract_point(data_slice): + x1_bytes = data_slice[:32] + y1_bytes = data_slice[32:64] + x2_i_bytes = data_slice[64:96] + x2_r_bytes = data_slice[96:128] + y2_i_bytes = data_slice[128:160] + y2_r_bytes = data_slice[160:192] + + x1 = big_endian_to_int(x1_bytes) + y1 = big_endian_to_int(y1_bytes) + x2_i = big_endian_to_int(x2_i_bytes) + x2_r = big_endian_to_int(x2_r_bytes) + y2_i = big_endian_to_int(y2_i_bytes) + y2_r = big_endian_to_int(y2_r_bytes) + + return x1, y1, x2_i, x2_r, y2_i, y2_r diff --git a/evm/precompiles/modexp.py b/evm/precompiles/modexp.py new file mode 100644 index 0000000000..fe6b94816c --- /dev/null +++ b/evm/precompiles/modexp.py @@ -0,0 +1,126 @@ +from evm import constants + +from evm.utils.numeric import ( + big_endian_to_int, + get_highest_bit_index, + int_to_big_endian, +) +from evm.utils.padding import ( + pad32r, + zpad_right, + zpad_left, +) + + +def _compute_adjusted_exponent_length(exponent_length, first_32_exponent_bytes): + exponent = big_endian_to_int(first_32_exponent_bytes) + + if exponent_length <= 32 and exponent == 0: + return 0 + elif exponent_length <= 32: + return get_highest_bit_index(exponent) + else: + first_32_bytes_as_int = big_endian_to_int(first_32_exponent_bytes) + return ( + 8 * (exponent_length - 32) + + get_highest_bit_index(first_32_bytes_as_int) + ) + + +def _compute_complexity(length): + if length <= 64: + return length ** 2 + elif length <= 1024: + return ( + length ** 2 // 4 + 96 * length - 3072 + ) + else: + return 2 ** 2 // 16 + 480 * length - 199680 + + +def _extract_lengths(data): + # extract argument lengths + base_length_bytes = pad32r(data[:32]) + base_length = big_endian_to_int(base_length_bytes) + + exponent_length_bytes = pad32r(data[32:64]) + exponent_length = big_endian_to_int(exponent_length_bytes) + + modulus_length_bytes = pad32r(data[64:96]) + modulus_length = big_endian_to_int(modulus_length_bytes) + + return base_length, exponent_length, modulus_length + + +def _compute_modexp_gas_fee(data): + base_length, exponent_length, modulus_length = _extract_lengths(data) + + first_32_exponent_bytes = zpad_right( + data[96 + base_length:96 + base_length + exponent_length], + to_size=min(exponent_length, 32), + )[:32] + adjusted_exponent_length = _compute_adjusted_exponent_length( + exponent_length, + first_32_exponent_bytes, + ) + complexity = _compute_complexity(max(modulus_length, base_length)) + + gas_fee = ( + complexity * + max(adjusted_exponent_length, 1) // + constants.GAS_MOD_EXP_QUADRATIC_DENOMINATOR + ) + return gas_fee + + +def _modexp(data): + base_length, exponent_length, modulus_length = _extract_lengths(data) + + if base_length == 0: + return 0 + elif modulus_length == 0: + return 0 + + # compute start:end indexes + base_end_idx = 96 + base_length + exponent_end_idx = base_end_idx + exponent_length + modulus_end_dx = exponent_end_idx + modulus_length + + # extract arguments + modulus_bytes = zpad_right( + data[exponent_end_idx:modulus_end_dx], + to_size=modulus_length, + ) + modulus = big_endian_to_int(modulus_bytes) + if modulus == 0: + return 0 + + base_bytes = zpad_right(data[96:base_end_idx], to_size=base_length) + base = big_endian_to_int(base_bytes) + + exponent_bytes = zpad_right( + data[base_end_idx:exponent_end_idx], + to_size=exponent_length, + ) + exponent = big_endian_to_int(exponent_bytes) + print('base', base, 'exponent', exponent, 'modulus', modulus) + + result = pow(base, exponent, modulus) + + return result + + +def modexp(computation): + """ + https://github.com/ethereum/EIPs/pull/198 + """ + gas_fee = _compute_modexp_gas_fee(computation.msg.data) + computation.gas_meter.consume_gas(gas_fee, reason='MODEXP Precompile') + + result = _modexp(computation.msg.data) + + _, _, modulus_length = _extract_lengths(computation.msg.data) + result_bytes = zpad_left(int_to_big_endian(result), to_size=modulus_length) + + computation.output = result_bytes + return computation diff --git a/evm/utils/bn128.py b/evm/utils/bn128.py new file mode 100644 index 0000000000..507af2d847 --- /dev/null +++ b/evm/utils/bn128.py @@ -0,0 +1,25 @@ +from py_ecc import ( + optimized_bn128 as bn128, +) + +from evm.exceptions import ( + ValidationError, +) + + +def validate_point(x, y): + FQ = bn128.FQ + + if x >= bn128.field_modulus: + raise ValidationError("Point x value is greater than field modulus") + elif y >= bn128.field_modulus: + raise ValidationError("Point y value is greater than field modulus") + + if (x, y) != (0, 0): + p1 = (FQ(x), FQ(y), FQ(1)) + if not bn128.is_on_curve(p1, bn128.b): + raise ValidationError("Point is not on the curve") + else: + p1 = (FQ(1), FQ(1), FQ(0)) + + return p1 diff --git a/evm/utils/numeric.py b/evm/utils/numeric.py index 283f53b53f..07ce355ad8 100644 --- a/evm/utils/numeric.py +++ b/evm/utils/numeric.py @@ -1,4 +1,5 @@ import functools +import itertools import math from evm.constants import ( @@ -62,3 +63,11 @@ def is_even(value): def is_odd(value): return value % 2 == 1 + + +def get_highest_bit_index(value): + value >>= 1 + for bit_length in itertools.count(): + if not value: + return bit_length + value >>= 1 diff --git a/evm/vm/computation.py b/evm/vm/computation.py index 6a62009b68..73984b321f 100644 --- a/evm/vm/computation.py +++ b/evm/vm/computation.py @@ -63,6 +63,7 @@ class Computation(object): children = None _output = b'' + return_data = b'' error = None logs = None @@ -157,11 +158,11 @@ def extend_memory(self, start_position, size): self.memory.extend(start_position, size) # - # Runtime Operations + # Computed properties. # @property def output(self): - if self.error: + if self.error and self.error.zeros_return_data: return b'' else: return self._output @@ -184,6 +185,18 @@ def apply_child_computation(self, child_msg): return child_computation def add_child_computation(self, child_computation): + if child_computation.error: + if child_computation.msg.is_create: + self.return_data = child_computation.output + elif child_computation.error.zeros_return_data: + self.return_data = b'' + else: + self.return_data = child_computation.output + else: + if child_computation.msg.is_create: + self.return_data = b'' + else: + self.return_data = child_computation.output self.children.append(child_computation) def register_account_for_deletion(self, beneficiary): @@ -231,7 +244,7 @@ def get_gas_refund(self): return self.gas_meter.gas_refunded + sum(c.get_gas_refund() for c in self.children) def get_gas_used(self): - if self.error: + if self.error and self.error.burns_gas: return self.msg.gas else: return max( @@ -240,7 +253,7 @@ def get_gas_used(self): ) def get_gas_remaining(self): - if self.error: + if self.error and self.error.burns_gas: return 0 else: return self.gas_meter.gas_remaining @@ -252,13 +265,14 @@ def __enter__(self): self.logger.debug( ( "COMPUTATION STARTING: gas: %s | from: %s | to: %s | value: %s " - "| depth %s" + "| depth %s | static: %s" ), self.msg.gas, encode_hex(self.msg.sender), encode_hex(self.msg.to), self.msg.value, self.msg.depth, + "y" if self.msg.is_static else "n", ) return self @@ -268,23 +282,25 @@ def __exit__(self, exc_type, exc_value, traceback): self.logger.debug( ( "COMPUTATION ERROR: gas: %s | from: %s | to: %s | value: %s | " - "depth: %s | error: %s" + "depth: %s | static: %s | error: %s" ), self.msg.gas, encode_hex(self.msg.sender), encode_hex(self.msg.to), self.msg.value, self.msg.depth, + "y" if self.msg.is_static else "n", exc_value, ) self.error = exc_value - self.gas_meter.consume_gas( - self.gas_meter.gas_remaining, - reason=" ".join(( - "Zeroing gas due to VM Exception:", - str(exc_value), - )), - ) + if self.error.burns_gas: + self.gas_meter.consume_gas( + self.gas_meter.gas_remaining, + reason=" ".join(( + "Zeroing gas due to VM Exception:", + str(exc_value), + )), + ) # suppress VM exceptions return True @@ -292,12 +308,13 @@ def __exit__(self, exc_type, exc_value, traceback): self.logger.debug( ( "COMPUTATION SUCCESS: from: %s | to: %s | value: %s | " - "depth: %s | gas-used: %s | gas-remaining: %s" + "depth: %s | static: %s | gas-used: %s | gas-remaining: %s" ), encode_hex(self.msg.sender), encode_hex(self.msg.to), self.msg.value, self.msg.depth, + "y" if self.msg.is_static else "n", self.msg.gas - self.gas_meter.gas_remaining, self.gas_meter.gas_remaining, ) diff --git a/evm/vm/forks/__init__.py b/evm/vm/forks/__init__.py index ec2e1dd317..7827822a2d 100644 --- a/evm/vm/forks/__init__.py +++ b/evm/vm/forks/__init__.py @@ -10,3 +10,6 @@ from .spurious_dragon import ( # noqa: F401 SpuriousDragonVM, ) +from .byzantium import ( # noqa: F401 + ByzantiumVM, +) diff --git a/evm/vm/forks/byzantium/__init__.py b/evm/vm/forks/byzantium/__init__.py new file mode 100644 index 0000000000..398aed313d --- /dev/null +++ b/evm/vm/forks/byzantium/__init__.py @@ -0,0 +1,59 @@ +from cytoolz import ( + merge, +) + +from evm import constants +from evm import precompiles +from evm.utils.address import ( + force_bytes_to_address, +) +from evm.validation import ( + validate_lte, +) + +from ..frontier import FRONTIER_PRECOMPILES +from ..spurious_dragon import SpuriousDragonVM + +from .headers import ( + create_byzantium_header_from_parent, + configure_byzantium_header, +) +from .opcodes import BYZANTIUM_OPCODES +from .blocks import ByzantiumBlock + + +BYZANTIUM_PRECOMPILES = merge( + FRONTIER_PRECOMPILES, + { + force_bytes_to_address(b'\x05'): precompiles.modexp, + force_bytes_to_address(b'\x06'): precompiles.ecadd, + force_bytes_to_address(b'\x07'): precompiles.ecmul, + force_bytes_to_address(b'\x08'): precompiles.ecpairing, + }, +) + + +def _byzantium_get_block_reward(block_number): + return constants.EIP649_BLOCK_REWARD + + +def _byzantium_get_uncle_reward(block_number, uncle): + validate_lte(uncle.block_number, constants.MAX_UNCLE_DEPTH) + block_number_delta = block_number - uncle.block_number + return (8 - block_number_delta) * constants.EIP649_BLOCK_REWARD // 8 + + +ByzantiumVM = SpuriousDragonVM.configure( + name='ByzantiumVM', + # precompiles + _precompiles=BYZANTIUM_PRECOMPILES, + # opcodes + opcodes=BYZANTIUM_OPCODES, + # RLP + _block_class=ByzantiumBlock, + # Methods + create_header_from_parent=staticmethod(create_byzantium_header_from_parent), + configure_header=configure_byzantium_header, + get_block_reward=staticmethod(_byzantium_get_block_reward), + get_uncle_reward=staticmethod(_byzantium_get_uncle_reward), +) diff --git a/evm/vm/forks/byzantium/blocks.py b/evm/vm/forks/byzantium/blocks.py new file mode 100644 index 0000000000..65aa07c687 --- /dev/null +++ b/evm/vm/forks/byzantium/blocks.py @@ -0,0 +1,34 @@ +from rlp.sedes import ( + CountableList, +) +from evm.rlp.headers import ( + BlockHeader, +) +from evm.rlp.receipts import ( + Receipt, +) +from evm.vm.forks.spurious_dragon.blocks import ( + SpuriousDragonBlock, +) + +from .transactions import ( + ByzantiumTransaction, +) + + +class ByzantiumBlock(SpuriousDragonBlock): + transaction_class = ByzantiumTransaction + fields = [ + ('header', BlockHeader), + ('transactions', CountableList(transaction_class)), + ('uncles', CountableList(BlockHeader)) + ] + + def make_receipt(self, transaction, computation): + old_receipt = super(ByzantiumBlock, self).make_receipt(transaction, computation) + receipt = Receipt( + state_root=b'' if computation.error else b'\x01', + gas_used=old_receipt.gas_used, + logs=old_receipt.logs, + ) + return receipt diff --git a/evm/vm/forks/byzantium/headers.py b/evm/vm/forks/byzantium/headers.py new file mode 100644 index 0000000000..a0a4c723a4 --- /dev/null +++ b/evm/vm/forks/byzantium/headers.py @@ -0,0 +1,78 @@ +from evm.constants import ( + EMPTY_UNCLE_HASH, + DIFFICULTY_ADJUSTMENT_DENOMINATOR, + DIFFICULTY_MINIMUM, + BOMB_EXPONENTIAL_PERIOD, + BOMB_EXPONENTIAL_FREE_PERIODS, + BYZANTIUM_DIFFICULTY_ADJUSTMENT_CUTOFF, +) +from evm.validation import ( + validate_gt, + validate_header_params_for_configuration, +) +from evm.vm.forks.frontier.headers import ( + create_frontier_header_from_parent, +) + + +def compute_byzantium_difficulty(parent_header, timestamp): + """ + https://github.com/ethereum/EIPs/issues/100 + TODO: figure out how to know about uncles in this context... + """ + parent_timestamp = parent_header.timestamp + validate_gt(timestamp, parent_timestamp, title="Header.timestamp") + + parent_difficulty = parent_header.difficulty + offset = parent_difficulty // DIFFICULTY_ADJUSTMENT_DENOMINATOR + + has_uncles = parent_header.uncles_hash != EMPTY_UNCLE_HASH + adj_factor = max( + ( + (2 if has_uncles else 1) - + ((timestamp - parent_timestamp) // BYZANTIUM_DIFFICULTY_ADJUSTMENT_CUTOFF) + ), + -99, + ) + difficulty = max( + parent_difficulty + offset * adj_factor, + min(parent_header.difficulty, DIFFICULTY_MINIMUM) + ) + num_bomb_periods = ( + max( + 0, + parent_header.block_number + 1 - 3000000, + ) // BOMB_EXPONENTIAL_PERIOD + ) - BOMB_EXPONENTIAL_FREE_PERIODS + + if num_bomb_periods >= 0: + return max(difficulty + 2**num_bomb_periods, DIFFICULTY_MINIMUM) + else: + return difficulty + + +def create_byzantium_header_from_parent(parent_header, **header_params): + if 'difficulty' not in header_params: + header_params.setdefault('timestamp', parent_header.timestamp + 1) + + header_params['difficulty'] = compute_byzantium_difficulty( + parent_header=parent_header, + timestamp=header_params['timestamp'], + ) + return create_frontier_header_from_parent(parent_header, **header_params) + + +def configure_byzantium_header(vm, **header_params): + validate_header_params_for_configuration(header_params) + + for field_name, value in header_params.items(): + setattr(vm.block.header, field_name, value) + + if 'timestamp' in header_params and vm.block.header.block_number > 0: + parent_header = vm.block.get_parent_header() + vm.block.header.difficulty = compute_byzantium_difficulty( + parent_header, + header_params['timestamp'], + ) + + return vm.block.header diff --git a/evm/vm/forks/byzantium/opcodes.py b/evm/vm/forks/byzantium/opcodes.py new file mode 100644 index 0000000000..6eed9b9f82 --- /dev/null +++ b/evm/vm/forks/byzantium/opcodes.py @@ -0,0 +1,126 @@ +import copy +import functools + +from cytoolz import merge + +from evm import constants +from evm.exceptions import ( + WriteProtection, +) +from evm import opcode_values +from evm import mnemonics + +from evm.opcode import as_opcode + +from evm.logic import ( + call, + context, + logging, + storage, + system, +) + +from evm.vm.forks.spurious_dragon.opcodes import SPURIOUS_DRAGON_OPCODES + + +def no_static(opcode_fn): + @functools.wraps(opcode_fn) + def inner(computation): + if computation.msg.is_static: + raise WriteProtection("Cannot modify state while inside of a STATICCALL context") + return opcode_fn(computation) + return inner + + +UPDATED_OPCODES = { + opcode_values.REVERT: as_opcode( + logic_fn=system.revert, + mnemonic=mnemonics.REVERT, + gas_cost=constants.GAS_ZERO, + ), + # + # Context + # + opcode_values.RETURNDATASIZE: as_opcode( + logic_fn=context.returndatasize, + mnemonic=mnemonics.RETURNDATASIZE, + gas_cost=constants.GAS_BASE, + ), + opcode_values.RETURNDATACOPY: as_opcode( + logic_fn=context.returndatacopy, + mnemonic=mnemonics.RETURNDATACOPY, + gas_cost=constants.GAS_VERYLOW, + ), + # + # Call + # + opcode_values.STATICCALL: call.StaticCall.configure( + name='opcode:STATICCALL', + mnemonic=mnemonics.STATICCALL, + gas_cost=constants.GAS_CALL_EIP150, + )(), + opcode_values.CALL: call.CallByzantium.configure( + name='opcode:CALL', + mnemonic=mnemonics.CALL, + gas_cost=constants.GAS_CALL_EIP150, + )(), + # + # Logging + # + opcode_values.LOG0: as_opcode( + logic_fn=no_static(logging.log0), + mnemonic=mnemonics.LOG0, + gas_cost=constants.GAS_LOG, + ), + opcode_values.LOG1: as_opcode( + logic_fn=no_static(logging.log1), + mnemonic=mnemonics.LOG1, + gas_cost=constants.GAS_LOG, + ), + opcode_values.LOG2: as_opcode( + logic_fn=no_static(logging.log2), + mnemonic=mnemonics.LOG2, + gas_cost=constants.GAS_LOG, + ), + opcode_values.LOG3: as_opcode( + logic_fn=no_static(logging.log3), + mnemonic=mnemonics.LOG3, + gas_cost=constants.GAS_LOG, + ), + opcode_values.LOG4: as_opcode( + logic_fn=no_static(logging.log4), + mnemonic=mnemonics.LOG4, + gas_cost=constants.GAS_LOG, + ), + # + # Create + # + opcode_values.CREATE: system.CreateByzantium.configure( + name='opcode:CREATE', + mnemonic=mnemonics.CREATE, + gas_cost=constants.GAS_CREATE, + )(), + # TODO: CREATE2 + # + # Storage + # + opcode_values.SSTORE: as_opcode( + logic_fn=no_static(storage.sstore), + mnemonic=mnemonics.SSTORE, + gas_cost=constants.GAS_NULL, + ), + # + # Self Destruct + # + opcode_values.SELFDESTRUCT: as_opcode( + logic_fn=no_static(system.selfdestruct_eip161), + mnemonic=mnemonics.SELFDESTRUCT, + gas_cost=constants.GAS_SELFDESTRUCT_EIP150, + ), +} + + +BYZANTIUM_OPCODES = merge( + copy.deepcopy(SPURIOUS_DRAGON_OPCODES), + UPDATED_OPCODES, +) diff --git a/evm/vm/forks/byzantium/transactions.py b/evm/vm/forks/byzantium/transactions.py new file mode 100644 index 0000000000..b06834c59e --- /dev/null +++ b/evm/vm/forks/byzantium/transactions.py @@ -0,0 +1,30 @@ +from evm.vm.forks.spurious_dragon.transactions import ( + SpuriousDragonTransaction, + SpuriousDragonUnsignedTransaction, +) + +from evm.utils.transactions import ( + create_transaction_signature, +) + + +class ByzantiumTransaction(SpuriousDragonTransaction): + @classmethod + def create_unsigned_transaction(cls, nonce, gas_price, gas, to, value, data): + return ByzantiumUnsignedTransaction(nonce, gas_price, gas, to, value, data) + + +class ByzantiumUnsignedTransaction(SpuriousDragonUnsignedTransaction): + def as_signed_transaction(self, private_key, chain_id=None): + v, r, s = create_transaction_signature(self, private_key, chain_id=chain_id) + return ByzantiumTransaction( + nonce=self.nonce, + gas_price=self.gas_price, + gas=self.gas, + to=self.to, + value=self.value, + data=self.data, + v=v, + r=r, + s=s, + ) diff --git a/evm/vm/forks/frontier/__init__.py b/evm/vm/forks/frontier/__init__.py index 4607493af1..187aa15617 100644 --- a/evm/vm/forks/frontier/__init__.py +++ b/evm/vm/forks/frontier/__init__.py @@ -135,37 +135,19 @@ def _execute_frontier_transaction(vm, transaction): # # 2) Post Computation # - if computation.error: - # Miner Fees - transaction_fee = transaction.gas * transaction.gas_price - vm.logger.debug('TRANSACTION FEE: %s', transaction_fee) - with vm.state_db() as state_db: - state_db.delta_balance(vm.block.header.coinbase, transaction_fee) - else: - # Self Destruct Refunds - num_deletions = len(computation.get_accounts_for_deletion()) - if num_deletions: - computation.gas_meter.refund_gas(constants.REFUND_SELFDESTRUCT * num_deletions) - - # Gas Refunds - gas_remaining = computation.get_gas_remaining() - gas_refunded = computation.get_gas_refund() - gas_used = transaction.gas - gas_remaining - gas_refund = min(gas_refunded, gas_used // 2) - gas_refund_amount = (gas_refund + gas_remaining) * transaction.gas_price - - if gas_refund_amount: - vm.logger.debug( - 'TRANSACTION REFUND: %s -> %s', - gas_refund_amount, - encode_hex(message.sender), - ) - - with vm.state_db() as state_db: - state_db.delta_balance(message.sender, gas_refund_amount) - - # Miner Fees - transaction_fee = (transaction.gas - gas_remaining - gas_refund) * transaction.gas_price + # Self Destruct Refunds + num_deletions = len(computation.get_accounts_for_deletion()) + if num_deletions: + computation.gas_meter.refund_gas(constants.REFUND_SELFDESTRUCT * num_deletions) + + # Gas Refunds + gas_remaining = computation.get_gas_remaining() + gas_refunded = computation.get_gas_refund() + gas_used = transaction.gas - gas_remaining + gas_refund = min(gas_refunded, gas_used // 2) + gas_refund_amount = (gas_refund + gas_remaining) * transaction.gas_price + + if gas_refund_amount: vm.logger.debug( 'TRANSACTION REFUND: %s -> %s', gas_refund_amount, @@ -173,7 +155,17 @@ def _execute_frontier_transaction(vm, transaction): ) with vm.state_db() as state_db: - state_db.delta_balance(vm.block.header.coinbase, transaction_fee) + state_db.delta_balance(message.sender, gas_refund_amount) + + # Miner Fees + transaction_fee = (transaction.gas - gas_remaining - gas_refund) * transaction.gas_price + vm.logger.debug( + 'TRANSACTION FEE: %s -> %s', + transaction_fee, + encode_hex(vm.block.header.coinbase), + ) + with vm.state_db() as state_db: + state_db.delta_balance(vm.block.header.coinbase, transaction_fee) # Process Self Destructs with vm.state_db() as state_db: diff --git a/evm/vm/message.py b/evm/vm/message.py index ebde292a47..e769b320e6 100644 --- a/evm/vm/message.py +++ b/evm/vm/message.py @@ -33,6 +33,7 @@ class Message(object): create_address = None should_transfer_value = None + is_static = None logger = logging.getLogger('evm.vm.message.Message') @@ -48,7 +49,8 @@ def __init__(self, depth=0, create_address=None, code_address=None, - should_transfer_value=True): + should_transfer_value=True, + is_static=False): validate_uint256(gas, title="Message.gas") self.gas = gas @@ -90,6 +92,9 @@ def __init__(self, validate_is_boolean(should_transfer_value, title="Message.should_transfer_value") self.should_transfer_value = should_transfer_value + validate_is_boolean(is_static, title="Message.is_static") + self.is_static = is_static + @property def is_origin(self): return self.sender == self.origin diff --git a/setup.py b/setup.py index 29887c829b..982aaf1a0b 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ "ethereum-bloom>=0.4.0", "ethereum-utils>=0.2.0", "pyethash>=0.1.27", + "py-ecc==1.4.2", "rlp==0.4.7", "ethereum-keys==0.1.0a7", "trie>=0.3.0", diff --git a/tests/core/numeric-utils/test_get_highest_bit_length.py b/tests/core/numeric-utils/test_get_highest_bit_length.py new file mode 100644 index 0000000000..94b65a4e7c --- /dev/null +++ b/tests/core/numeric-utils/test_get_highest_bit_length.py @@ -0,0 +1,20 @@ +import pytest + +from evm.utils.numeric import ( + get_highest_bit_index, +) + + +@pytest.mark.parametrize( + 'value,expected', + ( + (1, 0), + (2, 1), + (3, 1), + (255, 7), + (256, 8), + ) +) +def test_get_highest_bit_index(value, expected): + actual = get_highest_bit_index(value) + assert actual == expected diff --git a/tests/core/vm/test_computation.py b/tests/core/vm/test_computation.py index cf34f6ebaf..6b1401cabe 100644 --- a/tests/core/vm/test_computation.py +++ b/tests/core/vm/test_computation.py @@ -2,6 +2,7 @@ from evm.exceptions import ( VMError, + Revert, ) from evm.vm import ( Computation, @@ -185,6 +186,15 @@ def test_get_log_entries_with_vmerror(computation): assert computation.get_log_entries() == () +def test_get_log_entries_with_revert(computation): + # Trigger an out of gas error causing get log entries to be () + computation.add_log_entry(CANONICAL_ADDRESS_A, [1, 2, 3], b'') + with computation: + raise Revert('Triggered VMError for tests') + assert computation.error + assert computation.get_log_entries() == () + + def test_get_gas_refund(computation): computation.gas_meter.refund_gas(100) assert computation.get_gas_refund() == 100 @@ -199,6 +209,15 @@ def test_get_gas_refund_with_vmerror(computation): assert computation.get_gas_refund() == 0 +def test_get_gas_refund_with_revert(computation): + # Trigger an out of gas error causing get gas refund to be 0 + computation.gas_meter.refund_gas(100) + with computation: + raise Revert('Triggered VMError for tests') + assert computation.error + assert computation.get_gas_refund() == 0 + + def test_output(computation): computation.output = b'1' assert computation.output == b'1' @@ -213,6 +232,15 @@ def test_output_with_vmerror(computation): assert computation.output == b'' +def test_output_with_revert(computation): + # Trigger an out of gas error causing output to be b'' + computation.output = b'1' + with computation: + raise Revert('Triggered VMError for tests') + assert computation.error + assert computation.output == b'1' + + def test_get_gas_remaining(computation): assert computation.get_gas_remaining() == 100 @@ -226,6 +254,15 @@ def test_get_gas_remaining_with_vmerror(computation): assert computation.get_gas_remaining() == 0 +def test_get_gas_remaining_with_revert(computation): + assert computation.get_gas_remaining() == 100 + # Trigger an out of gas error causing get gas remaining to be 0 + with computation: + raise Revert('Triggered VMError for tests') + assert computation.error + assert computation.get_gas_remaining() == 100 + + def test_get_gas_used(computation): # User 3 gas to extend memory computation.gas_meter.consume_gas(3, reason='testing') @@ -241,3 +278,13 @@ def test_get_gas_used_with_vmerror(computation): raise VMError('Triggered VMError for tests') assert computation.error assert computation.get_gas_used() == 100 + + +def test_get_gas_used_with_revert(computation): + # Trigger an out of gas error causing get gas used to be 150 + computation.gas_meter.consume_gas(3, reason='testing') + computation.gas_meter.consume_gas(2, reason='testing') + with computation: + raise Revert('Triggered VMError for tests') + assert computation.error + assert computation.get_gas_used() == 5 diff --git a/tests/core/vm/test_modexp_precompile.py b/tests/core/vm/test_modexp_precompile.py new file mode 100644 index 0000000000..99e8face8e --- /dev/null +++ b/tests/core/vm/test_modexp_precompile.py @@ -0,0 +1,63 @@ +import pytest + +from evm.precompiles.modexp import ( + _modexp, + _compute_modexp_gas_fee, +) +from evm.utils.hexadecimal import ( + decode_hex, +) + + +EIP198_VECTOR_A = decode_hex( + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000000000000000000000000000000000000000000020" + "03" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" +) + +EIP198_VECTOR_B = decode_hex( + "0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000000000000000000000000000000000000000000020" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" +) + +EIP198_VECTOR_C = decode_hex( + "0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000020" + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe" + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd" +) + + +@pytest.mark.parametrize( + 'data,expected', + ( + (EIP198_VECTOR_A, 13056), + ( + EIP198_VECTOR_C, + 708647586132375115992254428253169996062012306153720251921480414128428353393856280, + ), + ), +) +def test_modexp_gas_fee_calcultation(data, expected): + actual = _compute_modexp_gas_fee(data) + assert actual == expected + + +@pytest.mark.parametrize( + 'data,expected', + ( + (EIP198_VECTOR_A, 1), + (EIP198_VECTOR_B, 0), + (EIP198_VECTOR_C, 0), + ), +) +def test_modexp_result(data, expected): + actual = _modexp(data) + assert actual == expected diff --git a/tests/json-fixtures/test_blockchain.py b/tests/json-fixtures/test_blockchain.py index 8633f03e2a..9488e112fe 100644 --- a/tests/json-fixtures/test_blockchain.py +++ b/tests/json-fixtures/test_blockchain.py @@ -20,6 +20,7 @@ FrontierVM, HomesteadVM as BaseHomesteadVM, SpuriousDragonVM, + ByzantiumVM, ) from evm.rlp.headers import ( BlockHeader, @@ -105,7 +106,9 @@ def chain_vm_configuration(fixture_data, fixture): (0, SpuriousDragonVM), ) elif network == 'Byzantium': - pytest.skip('Byzantium VM rules not yet supported') + return ( + (0, ByzantiumVM), + ) elif network == 'Constantinople': pytest.skip('Constantinople VM rules not yet supported') elif network == 'FrontierToHomesteadAt5': @@ -129,7 +132,10 @@ def chain_vm_configuration(fixture_data, fixture): (0, HomesteadVM), ) elif network == 'EIP158ToByzantiumAt5': - pytest.skip('Byzantium VM rules not yet supported') + return ( + (0, SpuriousDragonVM), + (5, ByzantiumVM), + ) else: fixture_path, fixture_key = fixture_data raise AssertionError( diff --git a/tests/json-fixtures/test_state.py b/tests/json-fixtures/test_state.py index b12d2a3b2e..ec52652088 100644 --- a/tests/json-fixtures/test_state.py +++ b/tests/json-fixtures/test_state.py @@ -22,6 +22,7 @@ FrontierVM, HomesteadVM, SpuriousDragonVM, + ByzantiumVM, ) from evm.rlp.headers import ( BlockHeader, @@ -116,6 +117,10 @@ def get_block_hash_for_testing(self, block_number): name='SpuriousDragonVMForTesting', get_ancestor_hash=get_block_hash_for_testing, ) +ByzantiumVMForTesting = ByzantiumVM.configure( + name='ByzantiumVMForTesting', + get_ancestor_hash=get_block_hash_for_testing, +) @pytest.fixture @@ -130,7 +135,7 @@ def fixture_vm_class(fixture_data): elif fork_name == 'EIP158': return SpuriousDragonVMForTesting elif fork_name == 'Byzantium': - pytest.skip("Byzantium VM has not been implemented") + return ByzantiumVMForTesting elif fork_name == 'Constantinople': pytest.skip("Constantinople VM has not been implemented") elif fork_name == 'Metropolis': From df89614348ee28faa79d60a5eb3ba08f84d8b311 Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Mon, 20 Nov 2017 06:47:41 -0700 Subject: [PATCH 2/3] minor PR cleanup --- evm/exceptions.py | 2 +- evm/logic/call.py | 6 +++--- evm/logic/system.py | 2 +- evm/vm/forks/byzantium/headers.py | 1 - evm/vm/forks/byzantium/opcodes.py | 16 ++++++++-------- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/evm/exceptions.py b/evm/exceptions.py index 3a83eb72b5..e02b04760b 100644 --- a/evm/exceptions.py +++ b/evm/exceptions.py @@ -130,7 +130,7 @@ class WriteProtection(VMError): class OutOfBoundsRead(VMError): """ - Error raised to indicate an attempt was made to read data beyond then + Error raised to indicate an attempt was made to read data beyond the boundaries of the buffer (such as with RETURNDATACOPY) """ pass diff --git a/evm/logic/call.py b/evm/logic/call.py index 7a393c8b78..6aa3daf549 100644 --- a/evm/logic/call.py +++ b/evm/logic/call.py @@ -154,7 +154,7 @@ def get_call_params(self, computation): memory_output_start_position, memory_output_size, True, # should_transfer_value, - computation.msg.is_static, # is_static + computation.msg.is_static, ) @@ -188,7 +188,7 @@ def get_call_params(self, computation): memory_output_start_position, memory_output_size, True, # should_transfer_value, - computation.msg.is_static, # is_static + computation.msg.is_static, ) @@ -225,7 +225,7 @@ def get_call_params(self, computation): memory_output_start_position, memory_output_size, False, # should_transfer_value, - computation.msg.is_static, # is_static + computation.msg.is_static, ) diff --git a/evm/logic/system.py b/evm/logic/system.py index 6cc50f3fd3..35e0347d85 100644 --- a/evm/logic/system.py +++ b/evm/logic/system.py @@ -37,7 +37,7 @@ def revert(computation): output = computation.memory.read(start_position, size) computation.output = bytes(output) - raise Revert(bytes(output)) + raise Revert(computation.output) def selfdestruct(computation): diff --git a/evm/vm/forks/byzantium/headers.py b/evm/vm/forks/byzantium/headers.py index a0a4c723a4..2f6d4f3e02 100644 --- a/evm/vm/forks/byzantium/headers.py +++ b/evm/vm/forks/byzantium/headers.py @@ -18,7 +18,6 @@ def compute_byzantium_difficulty(parent_header, timestamp): """ https://github.com/ethereum/EIPs/issues/100 - TODO: figure out how to know about uncles in this context... """ parent_timestamp = parent_header.timestamp validate_gt(timestamp, parent_timestamp, title="Header.timestamp") diff --git a/evm/vm/forks/byzantium/opcodes.py b/evm/vm/forks/byzantium/opcodes.py index 6eed9b9f82..1e69c4299c 100644 --- a/evm/vm/forks/byzantium/opcodes.py +++ b/evm/vm/forks/byzantium/opcodes.py @@ -23,7 +23,7 @@ from evm.vm.forks.spurious_dragon.opcodes import SPURIOUS_DRAGON_OPCODES -def no_static(opcode_fn): +def ensure_no_static(opcode_fn): @functools.wraps(opcode_fn) def inner(computation): if computation.msg.is_static: @@ -68,27 +68,27 @@ def inner(computation): # Logging # opcode_values.LOG0: as_opcode( - logic_fn=no_static(logging.log0), + logic_fn=ensure_no_static(logging.log0), mnemonic=mnemonics.LOG0, gas_cost=constants.GAS_LOG, ), opcode_values.LOG1: as_opcode( - logic_fn=no_static(logging.log1), + logic_fn=ensure_no_static(logging.log1), mnemonic=mnemonics.LOG1, gas_cost=constants.GAS_LOG, ), opcode_values.LOG2: as_opcode( - logic_fn=no_static(logging.log2), + logic_fn=ensure_no_static(logging.log2), mnemonic=mnemonics.LOG2, gas_cost=constants.GAS_LOG, ), opcode_values.LOG3: as_opcode( - logic_fn=no_static(logging.log3), + logic_fn=ensure_no_static(logging.log3), mnemonic=mnemonics.LOG3, gas_cost=constants.GAS_LOG, ), opcode_values.LOG4: as_opcode( - logic_fn=no_static(logging.log4), + logic_fn=ensure_no_static(logging.log4), mnemonic=mnemonics.LOG4, gas_cost=constants.GAS_LOG, ), @@ -105,7 +105,7 @@ def inner(computation): # Storage # opcode_values.SSTORE: as_opcode( - logic_fn=no_static(storage.sstore), + logic_fn=ensure_no_static(storage.sstore), mnemonic=mnemonics.SSTORE, gas_cost=constants.GAS_NULL, ), @@ -113,7 +113,7 @@ def inner(computation): # Self Destruct # opcode_values.SELFDESTRUCT: as_opcode( - logic_fn=no_static(system.selfdestruct_eip161), + logic_fn=ensure_no_static(system.selfdestruct_eip161), mnemonic=mnemonics.SELFDESTRUCT, gas_cost=constants.GAS_SELFDESTRUCT_EIP150, ), From 1a815fd3e9875611c0a8c518681a2a008b9ae514 Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Mon, 20 Nov 2017 06:47:56 -0700 Subject: [PATCH 3/3] unpack result before serializing in ECMUL and ECADD --- evm/precompiles/ecadd.py | 5 +++-- evm/precompiles/ecmul.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/evm/precompiles/ecadd.py b/evm/precompiles/ecadd.py index cbb7b2fb87..51351e0a73 100644 --- a/evm/precompiles/ecadd.py +++ b/evm/precompiles/ecadd.py @@ -28,9 +28,10 @@ def ecadd(computation): except ValidationError: raise VMError("Invalid ECADD parameters") + result_x, result_y = result result_bytes = b''.join(( - pad32(int_to_big_endian(result[0].n)), - pad32(int_to_big_endian(result[1].n)), + pad32(int_to_big_endian(result_x.n)), + pad32(int_to_big_endian(result_y.n)), )) computation.output = result_bytes return computation diff --git a/evm/precompiles/ecmul.py b/evm/precompiles/ecmul.py index fd02f9dc04..64af6df3a4 100644 --- a/evm/precompiles/ecmul.py +++ b/evm/precompiles/ecmul.py @@ -28,9 +28,10 @@ def ecmul(computation): except ValidationError: raise VMError("Invalid ECMUL parameters") + result_x, result_y = result result_bytes = b''.join(( - pad32(int_to_big_endian(result[0].n)), - pad32(int_to_big_endian(result[1].n)), + pad32(int_to_big_endian(result_x.n)), + pad32(int_to_big_endian(result_y.n)), )) computation.output = result_bytes return computation