diff --git a/eth/db/account.py b/eth/db/account.py index 9ff6f8d7da..7cf4957d29 100644 --- a/eth/db/account.py +++ b/eth/db/account.py @@ -239,11 +239,11 @@ def has_root(self, state_root: bytes) -> bool: # # Storage # - def get_storage(self, address, slot): + def get_storage(self, address, slot, from_journal=True): validate_canonical_address(address, title="Storage Address") validate_uint256(slot, title="Storage Slot") - account = self._get_account(address) + account = self._get_account(address, from_journal) storage = HashTrie(HexaryTrie(self._journaldb, account.storage_root)) slot_as_key = pad32(int_to_big_endian(slot)) @@ -375,8 +375,8 @@ def account_is_empty(self, address): # # Internal # - def _get_account(self, address): - rlp_account = self._journaltrie.get(address, b'') + def _get_account(self, address, from_journal=True): + rlp_account = (self._journaltrie if from_journal else self._trie_cache).get(address, b'') if rlp_account: account = rlp.decode(rlp_account, sedes=Account) else: diff --git a/eth/vm/computation.py b/eth/vm/computation.py index 6d34c9679a..26bfa62a22 100644 --- a/eth/vm/computation.py +++ b/eth/vm/computation.py @@ -130,7 +130,7 @@ def __init__(self, self._memory = Memory() self._stack = Stack() - self._gas_meter = GasMeter(message.gas) + self._gas_meter = self.get_gas_meter() self.children = [] self.accounts_to_delete = {} @@ -406,6 +406,9 @@ def add_log_entry(self, account: Address, topics: List[int], data: bytes) -> Non # # Getters # + def get_gas_meter(self) -> GasMeter: + return GasMeter(self.msg.gas) + def get_accounts_for_deletion(self) -> Tuple[Tuple[bytes, bytes], ...]: if self.is_error: return tuple() diff --git a/eth/vm/forks/constantinople/computation.py b/eth/vm/forks/constantinople/computation.py index 4652ebb0b9..4798edbba0 100644 --- a/eth/vm/forks/constantinople/computation.py +++ b/eth/vm/forks/constantinople/computation.py @@ -8,6 +8,10 @@ from eth.vm.forks.byzantium.computation import ( ByzantiumComputation ) +from eth.vm.gas_meter import ( + allow_negative_refund_strategy, + GasMeter, +) from .opcodes import CONSTANTINOPLE_OPCODES @@ -27,3 +31,9 @@ class ConstantinopleComputation(ByzantiumComputation): # Override opcodes = CONSTANTINOPLE_OPCODES _precompiles = CONSTANTINOPLE_PRECOMPILES + + def get_gas_meter(self) -> GasMeter: + return GasMeter( + self.msg.gas, + allow_negative_refund_strategy + ) diff --git a/eth/vm/forks/constantinople/constants.py b/eth/vm/forks/constantinople/constants.py index 0ba9138d13..3c9424f8bb 100644 --- a/eth/vm/forks/constantinople/constants.py +++ b/eth/vm/forks/constantinople/constants.py @@ -2,5 +2,11 @@ GAS_EXTCODEHASH_EIP1052 = 400 +GAS_SSTORE_EIP1283_NOOP = 200 +GAS_SSTORE_EIP1283_INIT = 20000 +GAS_SSTORE_EIP1283_CLEAN = 5000 +GAS_SSTORE_EIP1283_CLEAR_REFUND = 15000 +GAS_SSTORE_EIP1283_RESET_CLEAR_REFUND = 19800 +GAS_SSTORE_EIP1283_RESET_REFUND = 4800 EIP1234_BLOCK_REWARD = 2 * denoms.ether diff --git a/eth/vm/forks/constantinople/opcodes.py b/eth/vm/forks/constantinople/opcodes.py index 3975b06eb9..dce6409d05 100644 --- a/eth/vm/forks/constantinople/opcodes.py +++ b/eth/vm/forks/constantinople/opcodes.py @@ -16,6 +16,9 @@ from eth.vm.forks.constantinople.constants import ( GAS_EXTCODEHASH_EIP1052 ) +from eth.vm.forks.constantinople.storage import ( + sstore_eip1283, +) from eth.vm.logic import ( arithmetic, context, @@ -52,6 +55,11 @@ mnemonic=mnemonics.CREATE2, gas_cost=constants.GAS_CREATE, )(), + opcode_values.SSTORE: as_opcode( + logic_fn=sstore_eip1283, + mnemonic=mnemonics.SSTORE, + gas_cost=constants.GAS_NULL, + ), } CONSTANTINOPLE_OPCODES = merge( diff --git a/eth/vm/forks/constantinople/storage.py b/eth/vm/forks/constantinople/storage.py new file mode 100644 index 0000000000..4d99a1ae2a --- /dev/null +++ b/eth/vm/forks/constantinople/storage.py @@ -0,0 +1,73 @@ +from eth.constants import ( + UINT256 +) +from eth.vm.forks.constantinople import ( + constants +) + +from eth.utils.hexadecimal import ( + encode_hex, +) + + +def sstore_eip1283(computation): + slot, value = computation.stack_pop(num_items=2, type_hint=UINT256) + + current_value = computation.state.account_db.get_storage( + address=computation.msg.storage_address, + slot=slot, + ) + + original_value = computation.state.account_db.get_storage( + address=computation.msg.storage_address, + slot=slot, + from_journal=False + ) + + gas_refund = 0 + + if current_value == value: + gas_cost = constants.GAS_SSTORE_EIP1283_NOOP + else: + if original_value == current_value: + if original_value == 0: + gas_cost = constants.GAS_SSTORE_EIP1283_INIT + else: + gas_cost = constants.GAS_SSTORE_EIP1283_CLEAN + + if value == 0: + gas_refund += constants.GAS_SSTORE_EIP1283_CLEAR_REFUND + else: + gas_cost = constants.GAS_SSTORE_EIP1283_NOOP + + if original_value != 0: + if current_value == 0: + gas_refund -= constants.GAS_SSTORE_EIP1283_CLEAR_REFUND + if value == 0: + gas_refund += constants.GAS_SSTORE_EIP1283_CLEAR_REFUND + + if original_value == value: + if original_value == 0: + gas_refund += constants.GAS_SSTORE_EIP1283_RESET_CLEAR_REFUND + else: + gas_refund += constants.GAS_SSTORE_EIP1283_RESET_REFUND + + computation.consume_gas( + gas_cost, + reason="SSTORE: {0}[{1}] -> {2} (current: {3} / original: {4})".format( + encode_hex(computation.msg.storage_address), + slot, + value, + current_value, + original_value, + ) + ) + + if gas_refund: + computation.refund_gas(gas_refund) + + computation.state.account_db.set_storage( + address=computation.msg.storage_address, + slot=slot, + value=value, + ) diff --git a/eth/vm/gas_meter.py b/eth/vm/gas_meter.py index 5d7949469b..cd96ad6e1d 100644 --- a/eth/vm/gas_meter.py +++ b/eth/vm/gas_meter.py @@ -1,6 +1,7 @@ import logging from typing import ( - cast + Callable, + cast, ) from eth_utils import ( ValidationError, @@ -16,7 +17,22 @@ ) +def default_refund_strategy(gas_refunded_total: int, amount: int) -> int: + if amount < 0: + raise ValidationError("Gas refund amount must be positive") + + return gas_refunded_total + amount + + +def allow_negative_refund_strategy(gas_refunded_total: int, amount: int) -> int: + return gas_refunded_total + amount + + +RefundStrategy = Callable[[int, int], int] + + class GasMeter(object): + start_gas = None # type: int gas_refunded = None # type: int @@ -24,9 +40,12 @@ class GasMeter(object): logger = cast(TraceLogger, logging.getLogger('eth.gas.GasMeter')) - def __init__(self, start_gas: int) -> None: + def __init__(self, + start_gas: int, + refund_strategy: RefundStrategy = default_refund_strategy) -> None: validate_uint256(start_gas, title="Start Gas") + self.refund_strategy = refund_strategy self.start_gas = start_gas self.gas_remaining = self.start_gas @@ -70,10 +89,7 @@ def return_gas(self, amount: int) -> None: ) def refund_gas(self, amount: int) -> None: - if amount < 0: - raise ValidationError("Gas refund amount must be positive") - - self.gas_refunded += amount + self.gas_refunded = self.refund_strategy(self.gas_refunded, amount) self.logger.trace( 'GAS REFUND: %s + %s -> %s', diff --git a/tests/core/opcodes/test_opcodes.py b/tests/core/opcodes/test_opcodes.py index 854f1ea418..b3485660c1 100644 --- a/tests/core/opcodes/test_opcodes.py +++ b/tests/core/opcodes/test_opcodes.py @@ -51,15 +51,16 @@ ) -def prepare_computation(vm_class): +def setup_computation(vm_class, create_address, code): message = Message( to=CANONICAL_ADDRESS_A, sender=CANONICAL_ADDRESS_B, - value=100, + create_address=create_address, + value=0, data=b'', - code=b'', - gas=800, + code=code, + gas=1000000, ) tx_context = vm_class._state_class.transaction_context_class( @@ -75,6 +76,13 @@ def prepare_computation(vm_class): transaction_context=tx_context, ) + return computation + + +def prepare_general_computation(vm_class, create_address=None, code=b''): + + computation = setup_computation(vm_class, create_address, code) + computation.state.account_db.touch_account(decode_hex(EMPTY_ADDRESS_IN_STATE)) computation.state.account_db.set_code(decode_hex(ADDRESS_WITH_CODE[0]), ADDRESS_WITH_CODE[1]) @@ -92,7 +100,7 @@ def prepare_computation(vm_class): ) ) def test_add(vm_class, val1, val2, expected): - computation = prepare_computation(vm_class) + computation = prepare_general_computation(vm_class) computation.stack_push(val1) computation.stack_push(val2) computation.opcodes[opcode_values.ADD](computation) @@ -113,7 +121,7 @@ def test_add(vm_class, val1, val2, expected): ) ) def test_mul(vm_class, val1, val2, expected): - computation = prepare_computation(vm_class) + computation = prepare_general_computation(vm_class) computation.stack_push(val1) computation.stack_push(val2) computation.opcodes[opcode_values.MUL](computation) @@ -139,7 +147,7 @@ def test_mul(vm_class, val1, val2, expected): ) ) def test_exp(vm_class, base, exponent, expected): - computation = prepare_computation(vm_class) + computation = prepare_general_computation(vm_class) computation.stack_push(exponent) computation.stack_push(base) computation.opcodes[opcode_values.EXP](computation) @@ -222,7 +230,7 @@ def test_exp(vm_class, base, exponent, expected): ) ) def test_shl(vm_class, val1, val2, expected): - computation = prepare_computation(vm_class) + computation = prepare_general_computation(vm_class) computation.stack_push(decode_hex(val1)) computation.stack_push(decode_hex(val2)) computation.opcodes[opcode_values.SHL](computation) @@ -305,7 +313,7 @@ def test_shl(vm_class, val1, val2, expected): ) ) def test_shr(vm_class, val1, val2, expected): - computation = prepare_computation(vm_class) + computation = prepare_general_computation(vm_class) computation.stack_push(decode_hex(val1)) computation.stack_push(decode_hex(val2)) computation.opcodes[opcode_values.SHR](computation) @@ -418,7 +426,7 @@ def test_shr(vm_class, val1, val2, expected): ) ) def test_sar(vm_class, val1, val2, expected): - computation = prepare_computation(vm_class) + computation = prepare_general_computation(vm_class) computation.stack_push(decode_hex(val1)) computation.stack_push(decode_hex(val2)) computation.opcodes[opcode_values.SAR](computation) @@ -449,10 +457,162 @@ def test_sar(vm_class, val1, val2, expected): ) ) def test_extcodehash(vm_class, address, expected): - computation = prepare_computation(vm_class) + computation = prepare_general_computation(vm_class) computation.stack_push(decode_hex(address)) computation.opcodes[opcode_values.EXTCODEHASH](computation) result = computation.stack_pop(type_hint=constants.BYTES) assert encode_hex(pad32(result)) == expected + + +@pytest.mark.parametrize( + # Testcases from https://eips.ethereum.org/EIPS/eip-1283 + 'vm_class, code, gas_used, refund, original', + ( + ( + ByzantiumVM, + '0x60006000556000600055', + 10012, + 0, + 0, + ), + ( + ByzantiumVM, + '0x60006000556001600055', + 25012, + 0, + 0, + ), + ( + ConstantinopleVM, + '0x60006000556000600055', + 412, + 0, + 0, + ), + ( + ConstantinopleVM, + '0x60006000556001600055', + 20212, + 0, + 0, + ), + ( + ConstantinopleVM, + '0x60016000556000600055', + 20212, + 19800, + 0, + ), + ( + ConstantinopleVM, + '0x60016000556002600055', + 20212, + 0, + 0, + ), + ( + ConstantinopleVM, + '0x60016000556001600055', + 20212, + 0, + 0, + ), + ( + ConstantinopleVM, + '0x60006000556000600055', + 5212, + 15000, + 1, + ), + ( + ConstantinopleVM, + '0x60006000556001600055', + 5212, + 4800, + 1, + ), + ( + ConstantinopleVM, + '0x60006000556002600055', + 5212, + 0, + 1, + ), + ( + ConstantinopleVM, + '0x60026000556000600055', + 5212, + 15000, + 1, + ), + ( + ConstantinopleVM, + '0x60026000556003600055', + 5212, + 0, + 1, + ), + ( + ConstantinopleVM, + '0x60026000556001600055', + 5212, + 4800, + 1, + ), + ( + ConstantinopleVM, + '0x60026000556002600055', + 5212, + 0, + 1, + ), + ( + ConstantinopleVM, + '0x60016000556000600055', + 5212, + 15000, + 1, + ), + ( + ConstantinopleVM, + '0x60016000556002600055', + 5212, + 0, + 1, + ), + ( + ConstantinopleVM, + '0x60016000556001600055', + 412, + 0, + 1, + ), + ( + ConstantinopleVM, + '0x600160005560006000556001600055', + 40218, + 19800, + 0, + ), + ( + ConstantinopleVM, + '0x600060005560016000556000600055', + 10218, + 19800, + 1, + ), + ) +) +def test_sstore(vm_class, code, gas_used, refund, original): + + computation = setup_computation(vm_class, CANONICAL_ADDRESS_B, decode_hex(code)) + + computation.state.account_db.set_balance(CANONICAL_ADDRESS_B, 100000000000) + computation.state.account_db.set_storage(CANONICAL_ADDRESS_B, 0, original) + computation.state.account_db.persist() + + comp = computation.apply_message() + assert comp.get_gas_refund() == refund + assert comp.get_gas_used() == gas_used