diff --git a/src/ethereum/__init__.py b/src/ethereum/__init__.py index dc2195bb5e..7cfd43d838 100644 --- a/src/ethereum/__init__.py +++ b/src/ethereum/__init__.py @@ -17,7 +17,6 @@ possible, to aid in defining the behavior of Ethereum clients. """ import sys -from typing import Any __version__ = "0.1.0" @@ -26,12 +25,3 @@ # EVM_RECURSION_LIMIT = 1024 * 12 sys.setrecursionlimit(max(EVM_RECURSION_LIMIT, sys.getrecursionlimit())) - - -def evm_trace(evm: Any, op: Any) -> None: - """ - autoapi_noshow - Placeholder for an evm trace function. The spec does not trace evm by - default. EVM tracing will be injected if the user requests it. - """ - pass diff --git a/src/ethereum/arrow_glacier/fork.py b/src/ethereum/arrow_glacier/fork.py index 3dd1e26fc5..1ac4d6a66d 100644 --- a/src/ethereum/arrow_glacier/fork.py +++ b/src/ethereum/arrow_glacier/fork.py @@ -576,6 +576,7 @@ def apply_body( difficulty=block_difficulty, state=state, chain_id=chain_id, + traces=[], ) gas_used, logs, has_erred = process_transaction(env, tx) diff --git a/src/ethereum/arrow_glacier/utils/message.py b/src/ethereum/arrow_glacier/utils/message.py index 1b167677d9..f70e2be330 100644 --- a/src/ethereum/arrow_glacier/utils/message.py +++ b/src/ethereum/arrow_glacier/utils/message.py @@ -111,4 +111,5 @@ def prepare_message( is_static=is_static, accessed_addresses=accessed_addresses, accessed_storage_keys=set(preaccessed_storage_keys), + parent_evm=None, ) diff --git a/src/ethereum/arrow_glacier/vm/__init__.py b/src/ethereum/arrow_glacier/vm/__init__.py index e7d99a44a6..480c595506 100644 --- a/src/ethereum/arrow_glacier/vm/__init__.py +++ b/src/ethereum/arrow_glacier/vm/__init__.py @@ -44,6 +44,7 @@ class Environment: difficulty: Uint state: State chain_id: U64 + traces: List[dict] @dataclass @@ -65,6 +66,7 @@ class Message: is_static: bool accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/arrow_glacier/vm/gas.py b/src/ethereum/arrow_glacier/vm/gas.py index 35671a08a0..fea38b4b51 100644 --- a/src/ethereum/arrow_glacier/vm/gas.py +++ b/src/ethereum/arrow_glacier/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -106,6 +107,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/arrow_glacier/vm/instructions/storage.py b/src/ethereum/arrow_glacier/vm/instructions/storage.py index 6d090059a8..1f931c3427 100644 --- a/src/ethereum/arrow_glacier/vm/instructions/storage.py +++ b/src/ethereum/arrow_glacier/vm/instructions/storage.py @@ -95,8 +95,6 @@ def sstore(evm: Evm) -> None: else: gas_cost += GAS_WARM_ACCESS - charge_gas(evm, gas_cost) - # Refund Counter Calculation if current_value != new_value: if original_value != 0 and current_value != 0 and new_value == 0: @@ -118,6 +116,8 @@ def sstore(evm: Evm) -> None: GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS ) + charge_gas(evm, gas_cost) + # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/arrow_glacier/vm/instructions/system.py b/src/ethereum/arrow_glacier/vm/instructions/system.py index ec1f243857..edcbc0086e 100644 --- a/src/ethereum/arrow_glacier/vm/instructions/system.py +++ b/src/ethereum/arrow_glacier/vm/instructions/system.py @@ -114,6 +114,7 @@ def generic_create( is_static=False, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -283,6 +284,7 @@ def generic_call( is_static=True if is_staticcall else evm.message.is_static, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -468,17 +470,18 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if beneficiary not in evm.accessed_addresses: evm.accessed_addresses.add(beneficiary) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + charge_gas(evm, gas_cost) # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) diff --git a/src/ethereum/arrow_glacier/vm/interpreter.py b/src/ethereum/arrow_glacier/vm/interpreter.py index 83052f35a4..c4fc06b22c 100644 --- a/src/ethereum/arrow_glacier/vm/interpreter.py +++ b/src/ethereum/arrow_glacier/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -120,6 +129,11 @@ def process_message_call( touched_accounts = evm.touched_accounts refund_counter = U256(evm.refund_counter) + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) + return MessageCallOutput( gas_left=evm.gas_left, refund_counter=refund_counter, @@ -243,6 +257,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -267,8 +282,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -277,14 +293,19 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.output = b"" evm.has_erred = True except Revert as e: + evm_trace(evm, OpException()) evm.error = e evm.has_erred = True return evm diff --git a/src/ethereum/berlin/fork.py b/src/ethereum/berlin/fork.py index 57ab596f7c..bae2e0f73c 100644 --- a/src/ethereum/berlin/fork.py +++ b/src/ethereum/berlin/fork.py @@ -473,6 +473,7 @@ def apply_body( difficulty=block_difficulty, state=state, chain_id=chain_id, + traces=[], ) gas_used, logs, has_erred = process_transaction(env, tx) diff --git a/src/ethereum/berlin/utils/message.py b/src/ethereum/berlin/utils/message.py index 93fd152ef7..0297472096 100644 --- a/src/ethereum/berlin/utils/message.py +++ b/src/ethereum/berlin/utils/message.py @@ -111,4 +111,5 @@ def prepare_message( is_static=is_static, accessed_addresses=accessed_addresses, accessed_storage_keys=set(preaccessed_storage_keys), + parent_evm=None, ) diff --git a/src/ethereum/berlin/vm/__init__.py b/src/ethereum/berlin/vm/__init__.py index 652050eedd..e0cf80e647 100644 --- a/src/ethereum/berlin/vm/__init__.py +++ b/src/ethereum/berlin/vm/__init__.py @@ -43,6 +43,7 @@ class Environment: difficulty: Uint state: State chain_id: U64 + traces: List[dict] @dataclass @@ -64,6 +65,7 @@ class Message: is_static: bool accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/berlin/vm/gas.py b/src/ethereum/berlin/vm/gas.py index e576c35a4f..ac8d482faa 100644 --- a/src/ethereum/berlin/vm/gas.py +++ b/src/ethereum/berlin/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -107,6 +108,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/berlin/vm/instructions/storage.py b/src/ethereum/berlin/vm/instructions/storage.py index 6d090059a8..1f931c3427 100644 --- a/src/ethereum/berlin/vm/instructions/storage.py +++ b/src/ethereum/berlin/vm/instructions/storage.py @@ -95,8 +95,6 @@ def sstore(evm: Evm) -> None: else: gas_cost += GAS_WARM_ACCESS - charge_gas(evm, gas_cost) - # Refund Counter Calculation if current_value != new_value: if original_value != 0 and current_value != 0 and new_value == 0: @@ -118,6 +116,8 @@ def sstore(evm: Evm) -> None: GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS ) + charge_gas(evm, gas_cost) + # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/berlin/vm/instructions/system.py b/src/ethereum/berlin/vm/instructions/system.py index ec1f243857..52377d4cc6 100644 --- a/src/ethereum/berlin/vm/instructions/system.py +++ b/src/ethereum/berlin/vm/instructions/system.py @@ -46,6 +46,7 @@ GAS_SELF_DESTRUCT_NEW_ACCOUNT, GAS_WARM_ACCESS, GAS_ZERO, + REFUND_SELF_DESTRUCT, calculate_gas_extend_memory, calculate_message_call_gas, charge_gas, @@ -114,6 +115,7 @@ def generic_create( is_static=False, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -283,6 +285,7 @@ def generic_call( is_static=True if is_staticcall else evm.message.is_static, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -468,22 +471,33 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if beneficiary not in evm.accessed_addresses: evm.accessed_addresses.add(beneficiary) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + originator = evm.message.current_target + + refunded_accounts = evm.accounts_to_delete + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refunded_accounts.update(parent_evm.accounts_to_delete) + parent_evm = parent_evm.message.parent_evm + + if originator not in refunded_accounts: + evm.refund_counter += REFUND_SELF_DESTRUCT + + charge_gas(evm, gas_cost) # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) - originator = evm.message.current_target beneficiary_balance = get_account(evm.env.state, beneficiary).balance originator_balance = get_account(evm.env.state, originator).balance diff --git a/src/ethereum/berlin/vm/interpreter.py b/src/ethereum/berlin/vm/interpreter.py index ad0209c6f8..f32350f6e9 100644 --- a/src/ethereum/berlin/vm/interpreter.py +++ b/src/ethereum/berlin/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -33,7 +42,7 @@ touch_account, ) from ..vm import Message -from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, charge_gas +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import Environment, Evm from .exceptions import ( @@ -117,9 +126,12 @@ def process_message_call( logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + REFUND_SELF_DESTRUCT * len( - evm.accounts_to_delete - ) + refund_counter = U256(evm.refund_counter) + + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) return MessageCallOutput( gas_left=evm.gas_left, @@ -242,6 +254,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -266,8 +279,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -276,14 +290,19 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.output = b"" evm.has_erred = True except Revert as e: + evm_trace(evm, OpException()) evm.error = e evm.has_erred = True return evm diff --git a/src/ethereum/byzantium/fork.py b/src/ethereum/byzantium/fork.py index 5466e39473..2e2a869e61 100644 --- a/src/ethereum/byzantium/fork.py +++ b/src/ethereum/byzantium/fork.py @@ -461,6 +461,7 @@ def apply_body( time=block_time, difficulty=block_difficulty, state=state, + traces=[], ) gas_used, logs, has_erred = process_transaction(env, tx) diff --git a/src/ethereum/byzantium/utils/message.py b/src/ethereum/byzantium/utils/message.py index 9210075510..715e754182 100644 --- a/src/ethereum/byzantium/utils/message.py +++ b/src/ethereum/byzantium/utils/message.py @@ -93,4 +93,5 @@ def prepare_message( code_address=code_address, should_transfer_value=should_transfer_value, is_static=is_static, + parent_evm=None, ) diff --git a/src/ethereum/byzantium/vm/__init__.py b/src/ethereum/byzantium/vm/__init__.py index aa4f0d8b7f..bd0326f185 100644 --- a/src/ethereum/byzantium/vm/__init__.py +++ b/src/ethereum/byzantium/vm/__init__.py @@ -42,6 +42,7 @@ class Environment: time: U256 difficulty: Uint state: State + traces: List[dict] @dataclass @@ -61,6 +62,7 @@ class Message: depth: Uint should_transfer_value: bool is_static: bool + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/byzantium/vm/gas.py b/src/ethereum/byzantium/vm/gas.py index 35f3dea2f5..b1d71e5d34 100644 --- a/src/ethereum/byzantium/vm/gas.py +++ b/src/ethereum/byzantium/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -106,6 +107,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/byzantium/vm/instructions/storage.py b/src/ethereum/byzantium/vm/instructions/storage.py index 96897f50b8..12cd6b5325 100644 --- a/src/ethereum/byzantium/vm/instructions/storage.py +++ b/src/ethereum/byzantium/vm/instructions/storage.py @@ -73,11 +73,11 @@ def sstore(evm: Evm) -> None: else: gas_cost = GAS_STORAGE_UPDATE - charge_gas(evm, gas_cost) - if new_value == 0 and current_value != 0: evm.refund_counter += GAS_STORAGE_CLEAR_REFUND + charge_gas(evm, gas_cost) + # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/byzantium/vm/instructions/system.py b/src/ethereum/byzantium/vm/instructions/system.py index 0441db499a..b537870318 100644 --- a/src/ethereum/byzantium/vm/instructions/system.py +++ b/src/ethereum/byzantium/vm/instructions/system.py @@ -39,6 +39,7 @@ GAS_SELF_DESTRUCT, GAS_SELF_DESTRUCT_NEW_ACCOUNT, GAS_ZERO, + REFUND_SELF_DESTRUCT, calculate_gas_extend_memory, calculate_message_call_gas, charge_gas, @@ -118,6 +119,7 @@ def create(evm: Evm) -> None: code_address=None, should_transfer_value=True, is_static=False, + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -210,6 +212,7 @@ def generic_call( code_address=code_address, should_transfer_value=should_transfer_value, is_static=True if is_staticcall else evm.message.is_static, + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -381,18 +384,29 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + originator = evm.message.current_target + + refunded_accounts = evm.accounts_to_delete + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refunded_accounts.update(parent_evm.accounts_to_delete) + parent_evm = parent_evm.message.parent_evm + + if originator not in refunded_accounts: + evm.refund_counter += REFUND_SELF_DESTRUCT + + charge_gas(evm, gas_cost) # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) - originator = evm.message.current_target beneficiary_balance = get_account(evm.env.state, beneficiary).balance originator_balance = get_account(evm.env.state, originator).balance diff --git a/src/ethereum/byzantium/vm/interpreter.py b/src/ethereum/byzantium/vm/interpreter.py index 9bc03e7d97..6e4b708a58 100644 --- a/src/ethereum/byzantium/vm/interpreter.py +++ b/src/ethereum/byzantium/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -32,7 +41,7 @@ touch_account, ) from ..vm import Message -from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, charge_gas +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import Environment, Evm from .exceptions import ( @@ -116,9 +125,12 @@ def process_message_call( logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = evm.refund_counter + REFUND_SELF_DESTRUCT * len( - evm.accounts_to_delete - ) + refund_counter = evm.refund_counter + + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) return MessageCallOutput( gas_left=evm.gas_left, @@ -235,6 +247,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -257,8 +270,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -267,14 +281,19 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.output = b"" evm.has_erred = True except Revert as e: + evm_trace(evm, OpException()) evm.error = e evm.has_erred = True return evm diff --git a/src/ethereum/constantinople/fork.py b/src/ethereum/constantinople/fork.py index b4785b13f5..83c46f2583 100644 --- a/src/ethereum/constantinople/fork.py +++ b/src/ethereum/constantinople/fork.py @@ -461,6 +461,7 @@ def apply_body( time=block_time, difficulty=block_difficulty, state=state, + traces=[], ) gas_used, logs, has_erred = process_transaction(env, tx) diff --git a/src/ethereum/constantinople/utils/message.py b/src/ethereum/constantinople/utils/message.py index 19159b95ed..1cff60aa6a 100644 --- a/src/ethereum/constantinople/utils/message.py +++ b/src/ethereum/constantinople/utils/message.py @@ -93,4 +93,5 @@ def prepare_message( code_address=code_address, should_transfer_value=should_transfer_value, is_static=is_static, + parent_evm=None, ) diff --git a/src/ethereum/constantinople/vm/__init__.py b/src/ethereum/constantinople/vm/__init__.py index aa4f0d8b7f..bd0326f185 100644 --- a/src/ethereum/constantinople/vm/__init__.py +++ b/src/ethereum/constantinople/vm/__init__.py @@ -42,6 +42,7 @@ class Environment: time: U256 difficulty: Uint state: State + traces: List[dict] @dataclass @@ -61,6 +62,7 @@ class Message: depth: Uint should_transfer_value: bool is_static: bool + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/constantinople/vm/gas.py b/src/ethereum/constantinople/vm/gas.py index 480eb6c28b..55790d5975 100644 --- a/src/ethereum/constantinople/vm/gas.py +++ b/src/ethereum/constantinople/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -107,6 +108,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/constantinople/vm/instructions/storage.py b/src/ethereum/constantinople/vm/instructions/storage.py index 96897f50b8..12cd6b5325 100644 --- a/src/ethereum/constantinople/vm/instructions/storage.py +++ b/src/ethereum/constantinople/vm/instructions/storage.py @@ -73,11 +73,11 @@ def sstore(evm: Evm) -> None: else: gas_cost = GAS_STORAGE_UPDATE - charge_gas(evm, gas_cost) - if new_value == 0 and current_value != 0: evm.refund_counter += GAS_STORAGE_CLEAR_REFUND + charge_gas(evm, gas_cost) + # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/constantinople/vm/instructions/system.py b/src/ethereum/constantinople/vm/instructions/system.py index 0577cf9960..3e1cd24407 100644 --- a/src/ethereum/constantinople/vm/instructions/system.py +++ b/src/ethereum/constantinople/vm/instructions/system.py @@ -45,6 +45,7 @@ GAS_SELF_DESTRUCT, GAS_SELF_DESTRUCT_NEW_ACCOUNT, GAS_ZERO, + REFUND_SELF_DESTRUCT, calculate_gas_extend_memory, calculate_message_call_gas, charge_gas, @@ -109,6 +110,7 @@ def generic_create( code_address=None, should_transfer_value=True, is_static=False, + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -276,6 +278,7 @@ def generic_call( code_address=code_address, should_transfer_value=should_transfer_value, is_static=True if is_staticcall else evm.message.is_static, + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -447,18 +450,29 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + originator = evm.message.current_target + + refunded_accounts = evm.accounts_to_delete + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refunded_accounts.update(parent_evm.accounts_to_delete) + parent_evm = parent_evm.message.parent_evm + + if originator not in refunded_accounts: + evm.refund_counter += REFUND_SELF_DESTRUCT + + charge_gas(evm, gas_cost) # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) - originator = evm.message.current_target beneficiary_balance = get_account(evm.env.state, beneficiary).balance originator_balance = get_account(evm.env.state, originator).balance diff --git a/src/ethereum/constantinople/vm/interpreter.py b/src/ethereum/constantinople/vm/interpreter.py index 6332b4e61c..9d969fd147 100644 --- a/src/ethereum/constantinople/vm/interpreter.py +++ b/src/ethereum/constantinople/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -32,7 +41,7 @@ touch_account, ) from ..vm import Message -from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, charge_gas +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import Environment, Evm from .exceptions import ( @@ -116,9 +125,12 @@ def process_message_call( logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = evm.refund_counter + REFUND_SELF_DESTRUCT * len( - evm.accounts_to_delete - ) + refund_counter = evm.refund_counter + + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) return MessageCallOutput( gas_left=evm.gas_left, @@ -236,6 +248,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -258,8 +271,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -268,14 +282,19 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.output = b"" evm.has_erred = True except Revert as e: + evm_trace(evm, OpException()) evm.error = e evm.has_erred = True return evm diff --git a/src/ethereum/dao_fork/fork.py b/src/ethereum/dao_fork/fork.py index 0f1ccb9c59..33fa4f4bfa 100644 --- a/src/ethereum/dao_fork/fork.py +++ b/src/ethereum/dao_fork/fork.py @@ -466,6 +466,7 @@ def apply_body( time=block_time, difficulty=block_difficulty, state=state, + traces=[], ) gas_used, logs = process_transaction(env, tx) diff --git a/src/ethereum/dao_fork/utils/message.py b/src/ethereum/dao_fork/utils/message.py index 22746b6bef..31f8aae9a3 100644 --- a/src/ethereum/dao_fork/utils/message.py +++ b/src/ethereum/dao_fork/utils/message.py @@ -87,4 +87,5 @@ def prepare_message( current_target=current_target, code_address=code_address, should_transfer_value=should_transfer_value, + parent_evm=None, ) diff --git a/src/ethereum/dao_fork/vm/__init__.py b/src/ethereum/dao_fork/vm/__init__.py index dc1836b9da..7c3262d31f 100644 --- a/src/ethereum/dao_fork/vm/__init__.py +++ b/src/ethereum/dao_fork/vm/__init__.py @@ -41,6 +41,7 @@ class Environment: time: U256 difficulty: Uint state: State + traces: List[dict] @dataclass @@ -59,6 +60,7 @@ class Message: code: Bytes depth: Uint should_transfer_value: bool + parent_evm: Optional["Evm"] @dataclass @@ -79,4 +81,33 @@ class Evm: output: Bytes accounts_to_delete: Set[Address] has_erred: bool - children: List["Evm"] + + +def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of a successful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + """ + evm.gas_left += child_evm.gas_left + evm.logs += child_evm.logs + evm.refund_counter += child_evm.refund_counter + + +def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of an unsuccessful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + """ + evm.gas_left += child_evm.gas_left diff --git a/src/ethereum/dao_fork/vm/gas.py b/src/ethereum/dao_fork/vm/gas.py index 0b9f5fb121..4e0bf085ec 100644 --- a/src/ethereum/dao_fork/vm/gas.py +++ b/src/ethereum/dao_fork/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from ..fork_types import Address @@ -105,6 +106,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/dao_fork/vm/instructions/storage.py b/src/ethereum/dao_fork/vm/instructions/storage.py index c10261df77..cd579f0ba7 100644 --- a/src/ethereum/dao_fork/vm/instructions/storage.py +++ b/src/ethereum/dao_fork/vm/instructions/storage.py @@ -71,11 +71,11 @@ def sstore(evm: Evm) -> None: else: gas_cost = GAS_STORAGE_UPDATE - charge_gas(evm, gas_cost) - if new_value == 0 and current_value != 0: evm.refund_counter += GAS_STORAGE_CLEAR_REFUND + charge_gas(evm, gas_cost) + # OPERATION set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/dao_fork/vm/instructions/system.py b/src/ethereum/dao_fork/vm/instructions/system.py index 17dc8c207d..a1a7f04d67 100644 --- a/src/ethereum/dao_fork/vm/instructions/system.py +++ b/src/ethereum/dao_fork/vm/instructions/system.py @@ -21,11 +21,17 @@ set_account_balance, ) from ...utils.address import compute_contract_address, to_address -from .. import Evm, Message +from .. import ( + Evm, + Message, + incorporate_child_on_error, + incorporate_child_on_success, +) from ..gas import ( GAS_CALL, GAS_CREATE, GAS_ZERO, + REFUND_SELF_DESTRUCT, calculate_gas_extend_memory, calculate_message_call_gas, charge_gas, @@ -100,18 +106,18 @@ def create(evm: Evm) -> None: depth=evm.message.depth + 1, code_address=None, should_transfer_value=True, + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) - evm.children.append(child_evm) + if child_evm.has_erred: + incorporate_child_on_error(evm, child_evm) push(evm.stack, U256(0)) else: - evm.logs += child_evm.logs + incorporate_child_on_success(evm, child_evm) push( evm.stack, U256.from_be_bytes(child_evm.message.current_target) ) - evm.gas_left = child_evm.gas_left - child_evm.gas_left = Uint(0) # PROGRAM COUNTER evm.pc += 1 @@ -187,14 +193,15 @@ def generic_call( depth=evm.message.depth + 1, code_address=code_address, should_transfer_value=should_transfer_value, + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) - evm.children.append(child_evm) if child_evm.has_erred: + incorporate_child_on_error(evm, child_evm) push(evm.stack, U256(0)) else: - evm.logs += child_evm.logs + incorporate_child_on_success(evm, child_evm) push(evm.stack, U256(1)) actual_output_size = min(memory_output_size, U256(len(child_evm.output))) @@ -203,8 +210,6 @@ def generic_call( memory_output_start_position, child_evm.output[:actual_output_size], ) - evm.gas_left += child_evm.gas_left - child_evm.gas_left = Uint(0) def call(evm: Evm) -> None: @@ -338,10 +343,22 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS - pass + gas_cost = GAS_ZERO - # OPERATION originator = evm.message.current_target + + refunded_accounts = evm.accounts_to_delete + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refunded_accounts.update(parent_evm.accounts_to_delete) + parent_evm = parent_evm.message.parent_evm + + if originator not in refunded_accounts: + evm.refund_counter += REFUND_SELF_DESTRUCT + + charge_gas(evm, gas_cost) + + # OPERATION beneficiary_balance = get_account(evm.env.state, beneficiary).balance originator_balance = get_account(evm.env.state, originator).balance diff --git a/src/ethereum/dao_fork/vm/interpreter.py b/src/ethereum/dao_fork/vm/interpreter.py index 7439fd7599..0d1c6bfc15 100644 --- a/src/ethereum/dao_fork/vm/interpreter.py +++ b/src/ethereum/dao_fork/vm/interpreter.py @@ -12,11 +12,19 @@ A straightforward interpreter that executes EVM code. """ from dataclasses import dataclass -from itertools import chain from typing import Set, Tuple, Union -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ..fork_types import Address, Log from ..state import ( @@ -29,7 +37,7 @@ touch_account, ) from ..vm import Message -from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, charge_gas +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import Environment, Evm from .exceptions import ExceptionalHalt, InvalidOpcode, StackDepthLimitError @@ -91,16 +99,24 @@ def process_message_call( else: evm = process_message(message, env) - accounts_to_delete = collect_accounts_to_delete(evm) - refund_counter = ( - calculate_gas_refund(evm) - + len(accounts_to_delete) * REFUND_SELF_DESTRUCT + if evm.has_erred: + logs: Tuple[Log, ...] = () + accounts_to_delete = set() + refund_counter = U256(0) + else: + logs = evm.logs + accounts_to_delete = evm.accounts_to_delete + refund_counter = evm.refund_counter + + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred ) + evm_trace(evm, tx_end) return MessageCallOutput( gas_left=evm.gas_left, refund_counter=refund_counter, - logs=evm.logs if not evm.has_erred else (), + logs=logs, accounts_to_delete=accounts_to_delete, has_erred=evm.has_erred, ) @@ -200,6 +216,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -215,13 +232,13 @@ def execute_code(message: Message, env: Environment) -> Evm: output=b"", accounts_to_delete=set(), has_erred=False, - children=[], ) try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -230,61 +247,14 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.has_erred = True return evm - - -def collect_accounts_to_delete(evm: Evm) -> Set[Address]: - """ - Collects all the accounts that were marked for deletion by the - `SELFDESTRUCT` opcode. - - Parameters - ---------- - evm : - The current EVM frame. - - Returns - ------- - accounts_to_delete: `set` - returns all the accounts need marked for deletion by the - `SELFDESTRUCT` opcode. - """ - if evm.has_erred: - return set() - else: - return set( - chain( - evm.accounts_to_delete, - *(collect_accounts_to_delete(child) for child in evm.children), - ) - ) - - -def calculate_gas_refund(evm: Evm) -> U256: - """ - Adds up the gas that was refunded in each execution frame during the - message call. - - Parameters - ---------- - evm : - The current EVM frame. - - Returns - ------- - gas_refund: `ethereum.base_types.U256` - returns the total gas that needs to be refunded after executing the - message call. - """ - if evm.has_erred: - return U256(0) - else: - return evm.refund_counter + sum( - calculate_gas_refund(child_evm) for child_evm in evm.children - ) diff --git a/src/ethereum/frontier/fork.py b/src/ethereum/frontier/fork.py index 3a77c9d39b..b7bee8f4a1 100644 --- a/src/ethereum/frontier/fork.py +++ b/src/ethereum/frontier/fork.py @@ -447,6 +447,7 @@ def apply_body( time=block_time, difficulty=block_difficulty, state=state, + traces=[], ) gas_used, logs = process_transaction(env, tx) diff --git a/src/ethereum/frontier/utils/message.py b/src/ethereum/frontier/utils/message.py index 4611216324..12bd6c92ff 100644 --- a/src/ethereum/frontier/utils/message.py +++ b/src/ethereum/frontier/utils/message.py @@ -83,4 +83,5 @@ def prepare_message( depth=Uint(0), current_target=current_target, code_address=code_address, + parent_evm=None, ) diff --git a/src/ethereum/frontier/vm/__init__.py b/src/ethereum/frontier/vm/__init__.py index 7623c0c24f..100ae2c2b1 100644 --- a/src/ethereum/frontier/vm/__init__.py +++ b/src/ethereum/frontier/vm/__init__.py @@ -41,6 +41,7 @@ class Environment: time: U256 difficulty: Uint state: State + traces: List[dict] @dataclass @@ -58,6 +59,7 @@ class Message: code_address: Optional[Address] code: Bytes depth: Uint + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/frontier/vm/gas.py b/src/ethereum/frontier/vm/gas.py index 0b9f5fb121..4e0bf085ec 100644 --- a/src/ethereum/frontier/vm/gas.py +++ b/src/ethereum/frontier/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from ..fork_types import Address @@ -105,6 +106,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/frontier/vm/instructions/storage.py b/src/ethereum/frontier/vm/instructions/storage.py index c10261df77..cd579f0ba7 100644 --- a/src/ethereum/frontier/vm/instructions/storage.py +++ b/src/ethereum/frontier/vm/instructions/storage.py @@ -71,11 +71,11 @@ def sstore(evm: Evm) -> None: else: gas_cost = GAS_STORAGE_UPDATE - charge_gas(evm, gas_cost) - if new_value == 0 and current_value != 0: evm.refund_counter += GAS_STORAGE_CLEAR_REFUND + charge_gas(evm, gas_cost) + # OPERATION set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/frontier/vm/instructions/system.py b/src/ethereum/frontier/vm/instructions/system.py index 1a17942a56..93b6e24144 100644 --- a/src/ethereum/frontier/vm/instructions/system.py +++ b/src/ethereum/frontier/vm/instructions/system.py @@ -30,6 +30,7 @@ from ..gas import ( GAS_CREATE, GAS_ZERO, + REFUND_SELF_DESTRUCT, calculate_gas_extend_memory, calculate_message_call_gas, charge_gas, @@ -103,6 +104,7 @@ def create(evm: Evm) -> None: current_target=contract_address, depth=evm.message.depth + 1, code_address=None, + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -187,6 +189,7 @@ def generic_call( current_target=to, depth=evm.message.depth + 1, code_address=code_address, + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -334,10 +337,22 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS - pass + gas_cost = GAS_ZERO - # OPERATION originator = evm.message.current_target + + refunded_accounts = evm.accounts_to_delete + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refunded_accounts.update(parent_evm.accounts_to_delete) + parent_evm = parent_evm.message.parent_evm + + if originator not in refunded_accounts: + evm.refund_counter += REFUND_SELF_DESTRUCT + + charge_gas(evm, gas_cost) + + # OPERATION beneficiary_balance = get_account(evm.env.state, beneficiary).balance originator_balance = get_account(evm.env.state, originator).balance diff --git a/src/ethereum/frontier/vm/interpreter.py b/src/ethereum/frontier/vm/interpreter.py index 96d7d1b4c4..a8882df4f1 100644 --- a/src/ethereum/frontier/vm/interpreter.py +++ b/src/ethereum/frontier/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ..fork_types import Address, Log from ..state import ( @@ -29,7 +38,7 @@ touch_account, ) from ..vm import Message -from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, charge_gas +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import Environment, Evm from .exceptions import ExceptionalHalt, InvalidOpcode, StackDepthLimitError @@ -98,9 +107,12 @@ def process_message_call( else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete - refund_counter = evm.refund_counter + REFUND_SELF_DESTRUCT * len( - evm.accounts_to_delete - ) + refund_counter = evm.refund_counter + + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) return MessageCallOutput( gas_left=evm.gas_left, @@ -210,6 +222,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -229,8 +242,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -239,10 +253,14 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.has_erred = True return evm diff --git a/src/ethereum/gray_glacier/fork.py b/src/ethereum/gray_glacier/fork.py index a217666281..82d564d725 100644 --- a/src/ethereum/gray_glacier/fork.py +++ b/src/ethereum/gray_glacier/fork.py @@ -576,6 +576,7 @@ def apply_body( difficulty=block_difficulty, state=state, chain_id=chain_id, + traces=[], ) gas_used, logs, has_erred = process_transaction(env, tx) diff --git a/src/ethereum/gray_glacier/utils/message.py b/src/ethereum/gray_glacier/utils/message.py index d9ac180120..0f4ad6a364 100644 --- a/src/ethereum/gray_glacier/utils/message.py +++ b/src/ethereum/gray_glacier/utils/message.py @@ -111,4 +111,5 @@ def prepare_message( is_static=is_static, accessed_addresses=accessed_addresses, accessed_storage_keys=set(preaccessed_storage_keys), + parent_evm=None, ) diff --git a/src/ethereum/gray_glacier/vm/__init__.py b/src/ethereum/gray_glacier/vm/__init__.py index e7d99a44a6..480c595506 100644 --- a/src/ethereum/gray_glacier/vm/__init__.py +++ b/src/ethereum/gray_glacier/vm/__init__.py @@ -44,6 +44,7 @@ class Environment: difficulty: Uint state: State chain_id: U64 + traces: List[dict] @dataclass @@ -65,6 +66,7 @@ class Message: is_static: bool accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/gray_glacier/vm/gas.py b/src/ethereum/gray_glacier/vm/gas.py index 35671a08a0..fea38b4b51 100644 --- a/src/ethereum/gray_glacier/vm/gas.py +++ b/src/ethereum/gray_glacier/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -106,6 +107,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/gray_glacier/vm/instructions/storage.py b/src/ethereum/gray_glacier/vm/instructions/storage.py index 6d090059a8..1f931c3427 100644 --- a/src/ethereum/gray_glacier/vm/instructions/storage.py +++ b/src/ethereum/gray_glacier/vm/instructions/storage.py @@ -95,8 +95,6 @@ def sstore(evm: Evm) -> None: else: gas_cost += GAS_WARM_ACCESS - charge_gas(evm, gas_cost) - # Refund Counter Calculation if current_value != new_value: if original_value != 0 and current_value != 0 and new_value == 0: @@ -118,6 +116,8 @@ def sstore(evm: Evm) -> None: GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS ) + charge_gas(evm, gas_cost) + # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/gray_glacier/vm/instructions/system.py b/src/ethereum/gray_glacier/vm/instructions/system.py index ec1f243857..edcbc0086e 100644 --- a/src/ethereum/gray_glacier/vm/instructions/system.py +++ b/src/ethereum/gray_glacier/vm/instructions/system.py @@ -114,6 +114,7 @@ def generic_create( is_static=False, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -283,6 +284,7 @@ def generic_call( is_static=True if is_staticcall else evm.message.is_static, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -468,17 +470,18 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if beneficiary not in evm.accessed_addresses: evm.accessed_addresses.add(beneficiary) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + charge_gas(evm, gas_cost) # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) diff --git a/src/ethereum/gray_glacier/vm/interpreter.py b/src/ethereum/gray_glacier/vm/interpreter.py index 83052f35a4..c4fc06b22c 100644 --- a/src/ethereum/gray_glacier/vm/interpreter.py +++ b/src/ethereum/gray_glacier/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -120,6 +129,11 @@ def process_message_call( touched_accounts = evm.touched_accounts refund_counter = U256(evm.refund_counter) + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) + return MessageCallOutput( gas_left=evm.gas_left, refund_counter=refund_counter, @@ -243,6 +257,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -267,8 +282,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -277,14 +293,19 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.output = b"" evm.has_erred = True except Revert as e: + evm_trace(evm, OpException()) evm.error = e evm.has_erred = True return evm diff --git a/src/ethereum/homestead/fork.py b/src/ethereum/homestead/fork.py index 29da3e4a61..a649390a71 100644 --- a/src/ethereum/homestead/fork.py +++ b/src/ethereum/homestead/fork.py @@ -449,6 +449,7 @@ def apply_body( time=block_time, difficulty=block_difficulty, state=state, + traces=[], ) gas_used, logs = process_transaction(env, tx) diff --git a/src/ethereum/homestead/utils/message.py b/src/ethereum/homestead/utils/message.py index c9352e2b96..86406e1729 100644 --- a/src/ethereum/homestead/utils/message.py +++ b/src/ethereum/homestead/utils/message.py @@ -87,4 +87,5 @@ def prepare_message( current_target=current_target, code_address=code_address, should_transfer_value=should_transfer_value, + parent_evm=None, ) diff --git a/src/ethereum/homestead/vm/__init__.py b/src/ethereum/homestead/vm/__init__.py index 162a34eb9e..cc920efc86 100644 --- a/src/ethereum/homestead/vm/__init__.py +++ b/src/ethereum/homestead/vm/__init__.py @@ -41,6 +41,7 @@ class Environment: time: U256 difficulty: Uint state: State + traces: List[dict] @dataclass @@ -59,6 +60,7 @@ class Message: code: Bytes depth: Uint should_transfer_value: bool + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/homestead/vm/gas.py b/src/ethereum/homestead/vm/gas.py index 0b9f5fb121..4e0bf085ec 100644 --- a/src/ethereum/homestead/vm/gas.py +++ b/src/ethereum/homestead/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from ..fork_types import Address @@ -105,6 +106,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/homestead/vm/instructions/storage.py b/src/ethereum/homestead/vm/instructions/storage.py index c10261df77..cd579f0ba7 100644 --- a/src/ethereum/homestead/vm/instructions/storage.py +++ b/src/ethereum/homestead/vm/instructions/storage.py @@ -71,11 +71,11 @@ def sstore(evm: Evm) -> None: else: gas_cost = GAS_STORAGE_UPDATE - charge_gas(evm, gas_cost) - if new_value == 0 and current_value != 0: evm.refund_counter += GAS_STORAGE_CLEAR_REFUND + charge_gas(evm, gas_cost) + # OPERATION set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/homestead/vm/instructions/system.py b/src/ethereum/homestead/vm/instructions/system.py index 4f39153539..a1a7f04d67 100644 --- a/src/ethereum/homestead/vm/instructions/system.py +++ b/src/ethereum/homestead/vm/instructions/system.py @@ -31,6 +31,7 @@ GAS_CALL, GAS_CREATE, GAS_ZERO, + REFUND_SELF_DESTRUCT, calculate_gas_extend_memory, calculate_message_call_gas, charge_gas, @@ -105,6 +106,7 @@ def create(evm: Evm) -> None: depth=evm.message.depth + 1, code_address=None, should_transfer_value=True, + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -191,6 +193,7 @@ def generic_call( depth=evm.message.depth + 1, code_address=code_address, should_transfer_value=should_transfer_value, + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -340,10 +343,22 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS - pass + gas_cost = GAS_ZERO - # OPERATION originator = evm.message.current_target + + refunded_accounts = evm.accounts_to_delete + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refunded_accounts.update(parent_evm.accounts_to_delete) + parent_evm = parent_evm.message.parent_evm + + if originator not in refunded_accounts: + evm.refund_counter += REFUND_SELF_DESTRUCT + + charge_gas(evm, gas_cost) + + # OPERATION beneficiary_balance = get_account(evm.env.state, beneficiary).balance originator_balance = get_account(evm.env.state, originator).balance diff --git a/src/ethereum/homestead/vm/interpreter.py b/src/ethereum/homestead/vm/interpreter.py index b77ee218dd..16798682de 100644 --- a/src/ethereum/homestead/vm/interpreter.py +++ b/src/ethereum/homestead/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ..fork_types import Address, Log from ..state import ( @@ -29,7 +38,7 @@ touch_account, ) from ..vm import Message -from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, charge_gas +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import Environment, Evm from .exceptions import ExceptionalHalt, InvalidOpcode, StackDepthLimitError @@ -98,9 +107,12 @@ def process_message_call( else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete - refund_counter = evm.refund_counter + REFUND_SELF_DESTRUCT * len( - evm.accounts_to_delete - ) + refund_counter = evm.refund_counter + + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) return MessageCallOutput( gas_left=evm.gas_left, @@ -212,6 +224,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -231,8 +244,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -241,10 +255,14 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.has_erred = True return evm diff --git a/src/ethereum/istanbul/fork.py b/src/ethereum/istanbul/fork.py index 907f7efb74..083e48c85f 100644 --- a/src/ethereum/istanbul/fork.py +++ b/src/ethereum/istanbul/fork.py @@ -462,6 +462,7 @@ def apply_body( difficulty=block_difficulty, state=state, chain_id=chain_id, + traces=[], ) gas_used, logs, has_erred = process_transaction(env, tx) diff --git a/src/ethereum/istanbul/utils/message.py b/src/ethereum/istanbul/utils/message.py index a0e1b217b2..ff909f4c98 100644 --- a/src/ethereum/istanbul/utils/message.py +++ b/src/ethereum/istanbul/utils/message.py @@ -93,4 +93,5 @@ def prepare_message( code_address=code_address, should_transfer_value=should_transfer_value, is_static=is_static, + parent_evm=None, ) diff --git a/src/ethereum/istanbul/vm/__init__.py b/src/ethereum/istanbul/vm/__init__.py index d2f4942659..6c4a103c34 100644 --- a/src/ethereum/istanbul/vm/__init__.py +++ b/src/ethereum/istanbul/vm/__init__.py @@ -43,6 +43,7 @@ class Environment: difficulty: Uint state: State chain_id: U64 + traces: List[dict] @dataclass @@ -62,6 +63,7 @@ class Message: depth: Uint should_transfer_value: bool is_static: bool + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/istanbul/vm/gas.py b/src/ethereum/istanbul/vm/gas.py index 4bbf9a2c72..1ad30a3d1f 100644 --- a/src/ethereum/istanbul/vm/gas.py +++ b/src/ethereum/istanbul/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -109,6 +110,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/istanbul/vm/instructions/storage.py b/src/ethereum/istanbul/vm/instructions/storage.py index 60b7be60f2..dcf7a9a606 100644 --- a/src/ethereum/istanbul/vm/instructions/storage.py +++ b/src/ethereum/istanbul/vm/instructions/storage.py @@ -83,8 +83,6 @@ def sstore(evm: Evm) -> None: else: gas_cost = GAS_SLOAD - charge_gas(evm, gas_cost) - # Refund Counter Calculation if current_value != new_value: if original_value != 0 and current_value != 0 and new_value == 0: @@ -104,6 +102,8 @@ def sstore(evm: Evm) -> None: # Slot was originally non-empty and was UPDATED earlier evm.refund_counter += int(GAS_STORAGE_UPDATE - GAS_SLOAD) + charge_gas(evm, gas_cost) + # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/istanbul/vm/instructions/system.py b/src/ethereum/istanbul/vm/instructions/system.py index 0577cf9960..3e1cd24407 100644 --- a/src/ethereum/istanbul/vm/instructions/system.py +++ b/src/ethereum/istanbul/vm/instructions/system.py @@ -45,6 +45,7 @@ GAS_SELF_DESTRUCT, GAS_SELF_DESTRUCT_NEW_ACCOUNT, GAS_ZERO, + REFUND_SELF_DESTRUCT, calculate_gas_extend_memory, calculate_message_call_gas, charge_gas, @@ -109,6 +110,7 @@ def generic_create( code_address=None, should_transfer_value=True, is_static=False, + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -276,6 +278,7 @@ def generic_call( code_address=code_address, should_transfer_value=should_transfer_value, is_static=True if is_staticcall else evm.message.is_static, + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -447,18 +450,29 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + originator = evm.message.current_target + + refunded_accounts = evm.accounts_to_delete + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refunded_accounts.update(parent_evm.accounts_to_delete) + parent_evm = parent_evm.message.parent_evm + + if originator not in refunded_accounts: + evm.refund_counter += REFUND_SELF_DESTRUCT + + charge_gas(evm, gas_cost) # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) - originator = evm.message.current_target beneficiary_balance = get_account(evm.env.state, beneficiary).balance originator_balance = get_account(evm.env.state, originator).balance diff --git a/src/ethereum/istanbul/vm/interpreter.py b/src/ethereum/istanbul/vm/interpreter.py index 3aba445dd0..8af3401b94 100644 --- a/src/ethereum/istanbul/vm/interpreter.py +++ b/src/ethereum/istanbul/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -33,7 +42,7 @@ touch_account, ) from ..vm import Message -from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, charge_gas +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import Environment, Evm from .exceptions import ( @@ -117,9 +126,15 @@ def process_message_call( logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + REFUND_SELF_DESTRUCT * len( - evm.accounts_to_delete - ) + refund_counter = U256(evm.refund_counter) + # + REFUND_SELF_DESTRUCT * len( + # evm.accounts_to_delete + # ) + + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) return MessageCallOutput( gas_left=evm.gas_left, @@ -242,6 +257,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -264,8 +280,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -274,14 +291,19 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.output = b"" evm.has_erred = True except Revert as e: + evm_trace(evm, OpException()) evm.error = e evm.has_erred = True return evm diff --git a/src/ethereum/london/fork.py b/src/ethereum/london/fork.py index 68879cb290..b5a059af94 100644 --- a/src/ethereum/london/fork.py +++ b/src/ethereum/london/fork.py @@ -584,6 +584,7 @@ def apply_body( difficulty=block_difficulty, state=state, chain_id=chain_id, + traces=[], ) gas_used, logs, has_erred = process_transaction(env, tx) diff --git a/src/ethereum/london/utils/message.py b/src/ethereum/london/utils/message.py index ca9a6845bb..a82b33114e 100644 --- a/src/ethereum/london/utils/message.py +++ b/src/ethereum/london/utils/message.py @@ -111,4 +111,5 @@ def prepare_message( is_static=is_static, accessed_addresses=accessed_addresses, accessed_storage_keys=set(preaccessed_storage_keys), + parent_evm=None, ) diff --git a/src/ethereum/london/vm/__init__.py b/src/ethereum/london/vm/__init__.py index e7d99a44a6..480c595506 100644 --- a/src/ethereum/london/vm/__init__.py +++ b/src/ethereum/london/vm/__init__.py @@ -44,6 +44,7 @@ class Environment: difficulty: Uint state: State chain_id: U64 + traces: List[dict] @dataclass @@ -65,6 +66,7 @@ class Message: is_static: bool accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/london/vm/gas.py b/src/ethereum/london/vm/gas.py index 35671a08a0..fea38b4b51 100644 --- a/src/ethereum/london/vm/gas.py +++ b/src/ethereum/london/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -106,6 +107,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/london/vm/instructions/storage.py b/src/ethereum/london/vm/instructions/storage.py index 6d090059a8..1f931c3427 100644 --- a/src/ethereum/london/vm/instructions/storage.py +++ b/src/ethereum/london/vm/instructions/storage.py @@ -95,8 +95,6 @@ def sstore(evm: Evm) -> None: else: gas_cost += GAS_WARM_ACCESS - charge_gas(evm, gas_cost) - # Refund Counter Calculation if current_value != new_value: if original_value != 0 and current_value != 0 and new_value == 0: @@ -118,6 +116,8 @@ def sstore(evm: Evm) -> None: GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS ) + charge_gas(evm, gas_cost) + # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/london/vm/instructions/system.py b/src/ethereum/london/vm/instructions/system.py index ec1f243857..edcbc0086e 100644 --- a/src/ethereum/london/vm/instructions/system.py +++ b/src/ethereum/london/vm/instructions/system.py @@ -114,6 +114,7 @@ def generic_create( is_static=False, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -283,6 +284,7 @@ def generic_call( is_static=True if is_staticcall else evm.message.is_static, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -468,17 +470,18 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if beneficiary not in evm.accessed_addresses: evm.accessed_addresses.add(beneficiary) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + charge_gas(evm, gas_cost) # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) diff --git a/src/ethereum/london/vm/interpreter.py b/src/ethereum/london/vm/interpreter.py index 83052f35a4..c4fc06b22c 100644 --- a/src/ethereum/london/vm/interpreter.py +++ b/src/ethereum/london/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -120,6 +129,11 @@ def process_message_call( touched_accounts = evm.touched_accounts refund_counter = U256(evm.refund_counter) + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) + return MessageCallOutput( gas_left=evm.gas_left, refund_counter=refund_counter, @@ -243,6 +257,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -267,8 +282,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -277,14 +293,19 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.output = b"" evm.has_erred = True except Revert as e: + evm_trace(evm, OpException()) evm.error = e evm.has_erred = True return evm diff --git a/src/ethereum/muir_glacier/fork.py b/src/ethereum/muir_glacier/fork.py index e75c02fa9f..14f0c170a8 100644 --- a/src/ethereum/muir_glacier/fork.py +++ b/src/ethereum/muir_glacier/fork.py @@ -462,6 +462,7 @@ def apply_body( difficulty=block_difficulty, state=state, chain_id=chain_id, + traces=[], ) gas_used, logs, has_erred = process_transaction(env, tx) diff --git a/src/ethereum/muir_glacier/utils/message.py b/src/ethereum/muir_glacier/utils/message.py index 30b5a487f0..032b0a4525 100644 --- a/src/ethereum/muir_glacier/utils/message.py +++ b/src/ethereum/muir_glacier/utils/message.py @@ -93,4 +93,5 @@ def prepare_message( code_address=code_address, should_transfer_value=should_transfer_value, is_static=is_static, + parent_evm=None, ) diff --git a/src/ethereum/muir_glacier/vm/__init__.py b/src/ethereum/muir_glacier/vm/__init__.py index d2f4942659..6c4a103c34 100644 --- a/src/ethereum/muir_glacier/vm/__init__.py +++ b/src/ethereum/muir_glacier/vm/__init__.py @@ -43,6 +43,7 @@ class Environment: difficulty: Uint state: State chain_id: U64 + traces: List[dict] @dataclass @@ -62,6 +63,7 @@ class Message: depth: Uint should_transfer_value: bool is_static: bool + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/muir_glacier/vm/gas.py b/src/ethereum/muir_glacier/vm/gas.py index 4bbf9a2c72..1ad30a3d1f 100644 --- a/src/ethereum/muir_glacier/vm/gas.py +++ b/src/ethereum/muir_glacier/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -109,6 +110,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/muir_glacier/vm/instructions/storage.py b/src/ethereum/muir_glacier/vm/instructions/storage.py index 60b7be60f2..dcf7a9a606 100644 --- a/src/ethereum/muir_glacier/vm/instructions/storage.py +++ b/src/ethereum/muir_glacier/vm/instructions/storage.py @@ -83,8 +83,6 @@ def sstore(evm: Evm) -> None: else: gas_cost = GAS_SLOAD - charge_gas(evm, gas_cost) - # Refund Counter Calculation if current_value != new_value: if original_value != 0 and current_value != 0 and new_value == 0: @@ -104,6 +102,8 @@ def sstore(evm: Evm) -> None: # Slot was originally non-empty and was UPDATED earlier evm.refund_counter += int(GAS_STORAGE_UPDATE - GAS_SLOAD) + charge_gas(evm, gas_cost) + # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/muir_glacier/vm/instructions/system.py b/src/ethereum/muir_glacier/vm/instructions/system.py index 0577cf9960..3e1cd24407 100644 --- a/src/ethereum/muir_glacier/vm/instructions/system.py +++ b/src/ethereum/muir_glacier/vm/instructions/system.py @@ -45,6 +45,7 @@ GAS_SELF_DESTRUCT, GAS_SELF_DESTRUCT_NEW_ACCOUNT, GAS_ZERO, + REFUND_SELF_DESTRUCT, calculate_gas_extend_memory, calculate_message_call_gas, charge_gas, @@ -109,6 +110,7 @@ def generic_create( code_address=None, should_transfer_value=True, is_static=False, + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -276,6 +278,7 @@ def generic_call( code_address=code_address, should_transfer_value=should_transfer_value, is_static=True if is_staticcall else evm.message.is_static, + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -447,18 +450,29 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + originator = evm.message.current_target + + refunded_accounts = evm.accounts_to_delete + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refunded_accounts.update(parent_evm.accounts_to_delete) + parent_evm = parent_evm.message.parent_evm + + if originator not in refunded_accounts: + evm.refund_counter += REFUND_SELF_DESTRUCT + + charge_gas(evm, gas_cost) # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) - originator = evm.message.current_target beneficiary_balance = get_account(evm.env.state, beneficiary).balance originator_balance = get_account(evm.env.state, originator).balance diff --git a/src/ethereum/muir_glacier/vm/interpreter.py b/src/ethereum/muir_glacier/vm/interpreter.py index 19da996ab0..687804e83f 100644 --- a/src/ethereum/muir_glacier/vm/interpreter.py +++ b/src/ethereum/muir_glacier/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -33,7 +42,7 @@ touch_account, ) from ..vm import Message -from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, charge_gas +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import Environment, Evm from .exceptions import ( @@ -117,9 +126,12 @@ def process_message_call( logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = U256(evm.refund_counter) + REFUND_SELF_DESTRUCT * len( - evm.accounts_to_delete - ) + refund_counter = U256(evm.refund_counter) + + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) return MessageCallOutput( gas_left=evm.gas_left, @@ -242,6 +254,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -264,8 +277,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -274,14 +288,19 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.output = b"" evm.has_erred = True except Revert as e: + evm_trace(evm, OpException()) evm.error = e evm.has_erred = True return evm diff --git a/src/ethereum/paris/fork.py b/src/ethereum/paris/fork.py index 146a7687b1..e70b3fbd4d 100644 --- a/src/ethereum/paris/fork.py +++ b/src/ethereum/paris/fork.py @@ -487,6 +487,7 @@ def apply_body( prev_randao=prev_randao, state=state, chain_id=chain_id, + traces=[], ) gas_used, logs, has_erred = process_transaction(env, tx) diff --git a/src/ethereum/paris/utils/message.py b/src/ethereum/paris/utils/message.py index ad2db11221..ee8824bc07 100644 --- a/src/ethereum/paris/utils/message.py +++ b/src/ethereum/paris/utils/message.py @@ -111,4 +111,5 @@ def prepare_message( is_static=is_static, accessed_addresses=accessed_addresses, accessed_storage_keys=set(preaccessed_storage_keys), + parent_evm=None, ) diff --git a/src/ethereum/paris/vm/__init__.py b/src/ethereum/paris/vm/__init__.py index f381de1d42..093109c8c3 100644 --- a/src/ethereum/paris/vm/__init__.py +++ b/src/ethereum/paris/vm/__init__.py @@ -44,6 +44,7 @@ class Environment: prev_randao: Bytes32 state: State chain_id: U64 + traces: List[dict] @dataclass @@ -65,6 +66,7 @@ class Message: is_static: bool accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/paris/vm/gas.py b/src/ethereum/paris/vm/gas.py index 35671a08a0..fea38b4b51 100644 --- a/src/ethereum/paris/vm/gas.py +++ b/src/ethereum/paris/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -106,6 +107,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/paris/vm/instructions/storage.py b/src/ethereum/paris/vm/instructions/storage.py index 6d090059a8..1f931c3427 100644 --- a/src/ethereum/paris/vm/instructions/storage.py +++ b/src/ethereum/paris/vm/instructions/storage.py @@ -95,8 +95,6 @@ def sstore(evm: Evm) -> None: else: gas_cost += GAS_WARM_ACCESS - charge_gas(evm, gas_cost) - # Refund Counter Calculation if current_value != new_value: if original_value != 0 and current_value != 0 and new_value == 0: @@ -118,6 +116,8 @@ def sstore(evm: Evm) -> None: GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS ) + charge_gas(evm, gas_cost) + # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/paris/vm/instructions/system.py b/src/ethereum/paris/vm/instructions/system.py index ec1f243857..edcbc0086e 100644 --- a/src/ethereum/paris/vm/instructions/system.py +++ b/src/ethereum/paris/vm/instructions/system.py @@ -114,6 +114,7 @@ def generic_create( is_static=False, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -283,6 +284,7 @@ def generic_call( is_static=True if is_staticcall else evm.message.is_static, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -468,17 +470,18 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if beneficiary not in evm.accessed_addresses: evm.accessed_addresses.add(beneficiary) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + charge_gas(evm, gas_cost) # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) diff --git a/src/ethereum/paris/vm/interpreter.py b/src/ethereum/paris/vm/interpreter.py index b1f28a0e4e..84bf121a06 100644 --- a/src/ethereum/paris/vm/interpreter.py +++ b/src/ethereum/paris/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple, Union -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -120,6 +129,11 @@ def process_message_call( touched_accounts = evm.touched_accounts refund_counter = U256(evm.refund_counter) + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) + return MessageCallOutput( gas_left=evm.gas_left, refund_counter=refund_counter, @@ -243,6 +257,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -267,8 +282,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -277,14 +293,19 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.output = b"" evm.has_erred = True except Revert as e: + evm_trace(evm, OpException()) evm.error = e evm.has_erred = True return evm diff --git a/src/ethereum/shanghai/fork.py b/src/ethereum/shanghai/fork.py index de851687d2..6bf8859f13 100644 --- a/src/ethereum/shanghai/fork.py +++ b/src/ethereum/shanghai/fork.py @@ -499,6 +499,7 @@ def apply_body( prev_randao=prev_randao, state=state, chain_id=chain_id, + traces=[], ) gas_used, logs, has_erred = process_transaction(env, tx) diff --git a/src/ethereum/shanghai/utils/message.py b/src/ethereum/shanghai/utils/message.py index 725b17f129..77575b1572 100644 --- a/src/ethereum/shanghai/utils/message.py +++ b/src/ethereum/shanghai/utils/message.py @@ -111,4 +111,5 @@ def prepare_message( is_static=is_static, accessed_addresses=accessed_addresses, accessed_storage_keys=set(preaccessed_storage_keys), + parent_evm=None, ) diff --git a/src/ethereum/shanghai/vm/__init__.py b/src/ethereum/shanghai/vm/__init__.py index f381de1d42..093109c8c3 100644 --- a/src/ethereum/shanghai/vm/__init__.py +++ b/src/ethereum/shanghai/vm/__init__.py @@ -44,6 +44,7 @@ class Environment: prev_randao: Bytes32 state: State chain_id: U64 + traces: List[dict] @dataclass @@ -65,6 +66,7 @@ class Message: is_static: bool accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/shanghai/vm/gas.py b/src/ethereum/shanghai/vm/gas.py index 0eb757bbbf..d8ac492728 100644 --- a/src/ethereum/shanghai/vm/gas.py +++ b/src/ethereum/shanghai/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -107,6 +108,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/shanghai/vm/instructions/storage.py b/src/ethereum/shanghai/vm/instructions/storage.py index 6d090059a8..1f931c3427 100644 --- a/src/ethereum/shanghai/vm/instructions/storage.py +++ b/src/ethereum/shanghai/vm/instructions/storage.py @@ -95,8 +95,6 @@ def sstore(evm: Evm) -> None: else: gas_cost += GAS_WARM_ACCESS - charge_gas(evm, gas_cost) - # Refund Counter Calculation if current_value != new_value: if original_value != 0 and current_value != 0 and new_value == 0: @@ -118,6 +116,8 @@ def sstore(evm: Evm) -> None: GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS ) + charge_gas(evm, gas_cost) + # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/shanghai/vm/instructions/system.py b/src/ethereum/shanghai/vm/instructions/system.py index e96e6de321..d761ac9aa1 100644 --- a/src/ethereum/shanghai/vm/instructions/system.py +++ b/src/ethereum/shanghai/vm/instructions/system.py @@ -122,6 +122,7 @@ def generic_create( is_static=False, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -306,6 +307,7 @@ def generic_call( is_static=True if is_staticcall else evm.message.is_static, accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -491,17 +493,18 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if beneficiary not in evm.accessed_addresses: evm.accessed_addresses.add(beneficiary) - charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + gas_cost += GAS_COLD_ACCOUNT_ACCESS if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + charge_gas(evm, gas_cost) # OPERATION ensure(not evm.message.is_static, WriteInStaticContext) diff --git a/src/ethereum/shanghai/vm/interpreter.py b/src/ethereum/shanghai/vm/interpreter.py index 02440afa3c..f44ea1890d 100644 --- a/src/ethereum/shanghai/vm/interpreter.py +++ b/src/ethereum/shanghai/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple, Union -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -120,6 +129,11 @@ def process_message_call( touched_accounts = evm.touched_accounts refund_counter = U256(evm.refund_counter) + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) + return MessageCallOutput( gas_left=evm.gas_left, refund_counter=refund_counter, @@ -243,6 +257,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -267,8 +282,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -277,14 +293,19 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.output = b"" evm.has_erred = True except Revert as e: + evm_trace(evm, OpException()) evm.error = e evm.has_erred = True return evm diff --git a/src/ethereum/spurious_dragon/fork.py b/src/ethereum/spurious_dragon/fork.py index a80628ecb2..3be35f96f9 100644 --- a/src/ethereum/spurious_dragon/fork.py +++ b/src/ethereum/spurious_dragon/fork.py @@ -457,6 +457,7 @@ def apply_body( time=block_time, difficulty=block_difficulty, state=state, + traces=[], ) gas_used, logs = process_transaction(env, tx) diff --git a/src/ethereum/spurious_dragon/utils/message.py b/src/ethereum/spurious_dragon/utils/message.py index 13095049ed..9eb622f8b3 100644 --- a/src/ethereum/spurious_dragon/utils/message.py +++ b/src/ethereum/spurious_dragon/utils/message.py @@ -88,4 +88,5 @@ def prepare_message( current_target=current_target, code_address=code_address, should_transfer_value=should_transfer_value, + parent_evm=None, ) diff --git a/src/ethereum/spurious_dragon/vm/__init__.py b/src/ethereum/spurious_dragon/vm/__init__.py index 2dc6808c1e..ae5dd867a0 100644 --- a/src/ethereum/spurious_dragon/vm/__init__.py +++ b/src/ethereum/spurious_dragon/vm/__init__.py @@ -42,6 +42,7 @@ class Environment: time: U256 difficulty: Uint state: State + traces: List[dict] @dataclass @@ -60,6 +61,7 @@ class Message: code: Bytes depth: Uint should_transfer_value: bool + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/spurious_dragon/vm/gas.py b/src/ethereum/spurious_dragon/vm/gas.py index 316ad2ea2e..d5f8c04caa 100644 --- a/src/ethereum/spurious_dragon/vm/gas.py +++ b/src/ethereum/spurious_dragon/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -105,6 +106,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/spurious_dragon/vm/instructions/storage.py b/src/ethereum/spurious_dragon/vm/instructions/storage.py index c10261df77..cd579f0ba7 100644 --- a/src/ethereum/spurious_dragon/vm/instructions/storage.py +++ b/src/ethereum/spurious_dragon/vm/instructions/storage.py @@ -71,11 +71,11 @@ def sstore(evm: Evm) -> None: else: gas_cost = GAS_STORAGE_UPDATE - charge_gas(evm, gas_cost) - if new_value == 0 and current_value != 0: evm.refund_counter += GAS_STORAGE_CLEAR_REFUND + charge_gas(evm, gas_cost) + # OPERATION set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/spurious_dragon/vm/instructions/system.py b/src/ethereum/spurious_dragon/vm/instructions/system.py index e47e412789..7a45d823ed 100644 --- a/src/ethereum/spurious_dragon/vm/instructions/system.py +++ b/src/ethereum/spurious_dragon/vm/instructions/system.py @@ -37,6 +37,7 @@ GAS_SELF_DESTRUCT, GAS_SELF_DESTRUCT_NEW_ACCOUNT, GAS_ZERO, + REFUND_SELF_DESTRUCT, calculate_gas_extend_memory, calculate_message_call_gas, charge_gas, @@ -112,6 +113,7 @@ def create(evm: Evm) -> None: depth=evm.message.depth + 1, code_address=None, should_transfer_value=True, + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -198,6 +200,7 @@ def generic_call( depth=evm.message.depth + 1, code_address=code_address, should_transfer_value=should_transfer_value, + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -362,16 +365,27 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS + gas_cost = GAS_SELF_DESTRUCT if ( not is_account_alive(evm.env.state, beneficiary) and get_account(evm.env.state, evm.message.current_target).balance != 0 ): - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT) + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT - # OPERATION originator = evm.message.current_target + + refunded_accounts = evm.accounts_to_delete + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refunded_accounts.update(parent_evm.accounts_to_delete) + parent_evm = parent_evm.message.parent_evm + + if originator not in refunded_accounts: + evm.refund_counter += REFUND_SELF_DESTRUCT + + charge_gas(evm, gas_cost) + + # OPERATION beneficiary_balance = get_account(evm.env.state, beneficiary).balance originator_balance = get_account(evm.env.state, originator).balance diff --git a/src/ethereum/spurious_dragon/vm/interpreter.py b/src/ethereum/spurious_dragon/vm/interpreter.py index 417a7e8570..75508c724c 100644 --- a/src/ethereum/spurious_dragon/vm/interpreter.py +++ b/src/ethereum/spurious_dragon/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Iterable, Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ethereum.utils.ensure import ensure from ..fork_types import Address, Log @@ -32,7 +41,7 @@ touch_account, ) from ..vm import Message -from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, charge_gas +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import Environment, Evm from .exceptions import ( @@ -115,9 +124,12 @@ def process_message_call( logs = evm.logs accounts_to_delete = evm.accounts_to_delete touched_accounts = evm.touched_accounts - refund_counter = evm.refund_counter + REFUND_SELF_DESTRUCT * len( - evm.accounts_to_delete - ) + refund_counter = evm.refund_counter + + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) return MessageCallOutput( gas_left=evm.gas_left, @@ -233,6 +245,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -253,8 +266,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -263,10 +277,14 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.has_erred = True return evm diff --git a/src/ethereum/tangerine_whistle/fork.py b/src/ethereum/tangerine_whistle/fork.py index 29da3e4a61..a649390a71 100644 --- a/src/ethereum/tangerine_whistle/fork.py +++ b/src/ethereum/tangerine_whistle/fork.py @@ -449,6 +449,7 @@ def apply_body( time=block_time, difficulty=block_difficulty, state=state, + traces=[], ) gas_used, logs = process_transaction(env, tx) diff --git a/src/ethereum/tangerine_whistle/utils/message.py b/src/ethereum/tangerine_whistle/utils/message.py index 91162a69b2..b8771a5b3e 100644 --- a/src/ethereum/tangerine_whistle/utils/message.py +++ b/src/ethereum/tangerine_whistle/utils/message.py @@ -88,4 +88,5 @@ def prepare_message( current_target=current_target, code_address=code_address, should_transfer_value=should_transfer_value, + parent_evm=None, ) diff --git a/src/ethereum/tangerine_whistle/vm/__init__.py b/src/ethereum/tangerine_whistle/vm/__init__.py index 162a34eb9e..cc920efc86 100644 --- a/src/ethereum/tangerine_whistle/vm/__init__.py +++ b/src/ethereum/tangerine_whistle/vm/__init__.py @@ -41,6 +41,7 @@ class Environment: time: U256 difficulty: Uint state: State + traces: List[dict] @dataclass @@ -59,6 +60,7 @@ class Message: code: Bytes depth: Uint should_transfer_value: bool + parent_evm: Optional["Evm"] @dataclass diff --git a/src/ethereum/tangerine_whistle/vm/gas.py b/src/ethereum/tangerine_whistle/vm/gas.py index 5be7a7e65f..60779cf74f 100644 --- a/src/ethereum/tangerine_whistle/vm/gas.py +++ b/src/ethereum/tangerine_whistle/vm/gas.py @@ -15,6 +15,7 @@ from typing import List, Tuple from ethereum.base_types import U256, Uint +from ethereum.trace import GasAndRefund, evm_trace from ethereum.utils.numeric import ceil32 from . import Evm @@ -105,6 +106,8 @@ def charge_gas(evm: Evm, amount: Uint) -> None: The amount of gas the current operation requires. """ + evm_trace(evm, GasAndRefund(amount)) + if evm.gas_left < amount: raise OutOfGasError else: diff --git a/src/ethereum/tangerine_whistle/vm/instructions/storage.py b/src/ethereum/tangerine_whistle/vm/instructions/storage.py index c10261df77..cd579f0ba7 100644 --- a/src/ethereum/tangerine_whistle/vm/instructions/storage.py +++ b/src/ethereum/tangerine_whistle/vm/instructions/storage.py @@ -71,11 +71,11 @@ def sstore(evm: Evm) -> None: else: gas_cost = GAS_STORAGE_UPDATE - charge_gas(evm, gas_cost) - if new_value == 0 and current_value != 0: evm.refund_counter += GAS_STORAGE_CLEAR_REFUND + charge_gas(evm, gas_cost) + # OPERATION set_storage(evm.env.state, evm.message.current_target, key, new_value) diff --git a/src/ethereum/tangerine_whistle/vm/instructions/system.py b/src/ethereum/tangerine_whistle/vm/instructions/system.py index d8011cc75f..014047b34d 100644 --- a/src/ethereum/tangerine_whistle/vm/instructions/system.py +++ b/src/ethereum/tangerine_whistle/vm/instructions/system.py @@ -36,6 +36,7 @@ GAS_SELF_DESTRUCT, GAS_SELF_DESTRUCT_NEW_ACCOUNT, GAS_ZERO, + REFUND_SELF_DESTRUCT, calculate_gas_extend_memory, calculate_message_call_gas, charge_gas, @@ -111,6 +112,7 @@ def create(evm: Evm) -> None: depth=evm.message.depth + 1, code_address=None, should_transfer_value=True, + parent_evm=evm, ) child_evm = process_create_message(child_message, evm.env) @@ -197,6 +199,7 @@ def generic_call( depth=evm.message.depth + 1, code_address=code_address, should_transfer_value=should_transfer_value, + parent_evm=evm, ) child_evm = process_message(child_message, evm.env) @@ -358,13 +361,24 @@ def selfdestruct(evm: Evm) -> None: beneficiary = to_address(pop(evm.stack)) # GAS - if account_exists(evm.env.state, beneficiary): - charge_gas(evm, GAS_SELF_DESTRUCT) - else: - charge_gas(evm, GAS_SELF_DESTRUCT + GAS_SELF_DESTRUCT_NEW_ACCOUNT) + gas_cost = GAS_SELF_DESTRUCT + if not account_exists(evm.env.state, beneficiary): + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT - # OPERATION originator = evm.message.current_target + + refunded_accounts = evm.accounts_to_delete + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refunded_accounts.update(parent_evm.accounts_to_delete) + parent_evm = parent_evm.message.parent_evm + + if originator not in refunded_accounts: + evm.refund_counter += REFUND_SELF_DESTRUCT + + charge_gas(evm, gas_cost) + + # OPERATION beneficiary_balance = get_account(evm.env.state, beneficiary).balance originator_balance = get_account(evm.env.state, originator).balance diff --git a/src/ethereum/tangerine_whistle/vm/interpreter.py b/src/ethereum/tangerine_whistle/vm/interpreter.py index e9fce95604..f27c7f5414 100644 --- a/src/ethereum/tangerine_whistle/vm/interpreter.py +++ b/src/ethereum/tangerine_whistle/vm/interpreter.py @@ -14,8 +14,17 @@ from dataclasses import dataclass from typing import Set, Tuple -from ethereum import evm_trace from ethereum.base_types import U256, Bytes0, Uint +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) from ..fork_types import Address, Log from ..state import ( @@ -29,7 +38,7 @@ touch_account, ) from ..vm import Message -from ..vm.gas import GAS_CODE_DEPOSIT, REFUND_SELF_DESTRUCT, charge_gas +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import Environment, Evm from .exceptions import ExceptionalHalt, InvalidOpcode, StackDepthLimitError @@ -98,9 +107,12 @@ def process_message_call( else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete - refund_counter = evm.refund_counter + REFUND_SELF_DESTRUCT * len( - evm.accounts_to_delete - ) + refund_counter = evm.refund_counter + + tx_end = TransactionEnd( + message.gas - evm.gas_left, evm.output, evm.has_erred + ) + evm_trace(evm, tx_end) return MessageCallOutput( gas_left=evm.gas_left, @@ -212,6 +224,7 @@ def execute_code(message: Message, env: Environment) -> Evm: """ code = message.code valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( pc=Uint(0), stack=[], @@ -231,8 +244,9 @@ def execute_code(message: Message, env: Environment) -> Evm: try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: - evm_trace(evm, evm.message.code_address) + evm_trace(evm, PrecompileStart(evm.message.code_address)) PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) return evm while evm.running and evm.pc < len(evm.code): @@ -241,10 +255,14 @@ def execute_code(message: Message, env: Environment) -> Evm: except ValueError: raise InvalidOpcode(evm.code[evm.pc]) - evm_trace(evm, op) + evm_trace(evm, OpStart(op)) op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) except ExceptionalHalt: + evm_trace(evm, OpException()) evm.gas_left = Uint(0) evm.has_erred = True return evm diff --git a/src/ethereum/trace.py b/src/ethereum/trace.py new file mode 100644 index 0000000000..36642d55e6 --- /dev/null +++ b/src/ethereum/trace.py @@ -0,0 +1,104 @@ +""" +.. _trace: + +EVM Trace +^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Defines the functions required for creating evm traces during execution. +""" + +import enum +from dataclasses import dataclass +from typing import Union + + +@dataclass +class TransactionStart: + """Trace event that is triggered at the start of a transaction.""" + + pass + + +@dataclass +class TransactionEnd: + """Trace event that is triggered at the end of a transaction.""" + + gas_used: int + output: bytes + has_erred: bool + + +@dataclass +class PrecompileStart: + """Trace event that is triggered before executing a precompile.""" + + address: bytes + + +@dataclass +class PrecompileEnd: + """Trace event that is triggered after executing a precompile.""" + + pass + + +@dataclass +class OpStart: + """Trace event that is triggered before executing an opcode.""" + + op: enum.Enum + + +@dataclass +class OpEnd: + """Trace event that is triggered after executing an opcode.""" + + pass + + +@dataclass +class OpException: + """Trace event that is triggered when an opcode raises an exception.""" + + pass + + +@dataclass +class EvmStop: + """Trace event that is triggered when the EVM stops.""" + + op: enum.Enum + + +@dataclass +class GasAndRefund: + """Trace event that is triggered when gas is deducted.""" + + gas_cost: int + + +TraceEvent = Union[ + TransactionStart, + TransactionEnd, + PrecompileStart, + PrecompileEnd, + OpStart, + OpEnd, + OpException, + EvmStop, + GasAndRefund, +] + + +def evm_trace(evm: object, event: TraceEvent) -> None: + """ + Create a trace of the event. + """ + pass diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 2f9e6ff3b6..4d2617bc0a 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -8,7 +8,7 @@ import sys from typing import Any -from ethereum import rlp +from ethereum import rlp, trace from ethereum.base_types import U64, U256, Uint from ethereum.crypto.hash import keccak256 from ethereum_spec_tools.forks import Hardfork @@ -21,6 +21,7 @@ parse_hex_or_int, ) from .env import Env +from .evm_trace import evm_trace, output_traces from .t8n_types import Alloc, Result, Txs @@ -62,24 +63,12 @@ def t8n_arguments(subparsers: argparse._SubParsersAction) -> None: "--state.reward", dest="state_reward", type=int, default=0 ) # TODO: Add support for the following trace options - t8n_parser.add_argument( - "--trace.memory", dest="trace_memory", type=bool, default=False - ) - t8n_parser.add_argument( - "--trace.nomemory", dest="trace_nomemory", type=bool, default=True - ) - t8n_parser.add_argument( - "--trace.noreturndata", - dest="trace_noreturndata", - type=bool, - default=True, - ) - t8n_parser.add_argument( - "--trace.nostack ", dest="trace_nostack ", type=bool, default=False - ) - t8n_parser.add_argument( - "--trace.returndata", dest="trace_returndata", type=bool, default=False - ) + t8n_parser.add_argument("--trace", action="store_true") + t8n_parser.add_argument("--trace.memory", action="store_true") + t8n_parser.add_argument("--trace.nomemory", action="store_true") + t8n_parser.add_argument("--trace.noreturndata", action="store_true") + t8n_parser.add_argument("--trace.nostack ", action="store_true") + t8n_parser.add_argument("--trace.returndata", action="store_true") class T8N(Load): @@ -102,6 +91,8 @@ def __init__(self, options: Any) -> None: self.forks, self.options, stdin ) + if self.options.trace: + trace.evm_trace = evm_trace self.logger = get_stream_logger("T8N") super().__init__( @@ -230,6 +221,8 @@ def environment(self, tx: Any, gas_available: Any) -> Any: kw_arguments["caller"] = kw_arguments["origin"] = sender_address kw_arguments["gas_price"] = tx.gas_price + kw_arguments["traces"] = [] + return self.vm.Environment(**kw_arguments) def tx_trie_set(self, trie: Any, index: Any, tx: Any) -> Any: @@ -340,6 +333,11 @@ def apply_body(self) -> None: gas_consumed = process_transaction_return[0] gas_available -= gas_consumed + if self.options.trace: + tx_hash = self.txs.get_tx_hash(tx) + output_traces( + env.traces, i, tx_hash, self.options.output_basedir + ) self.tx_trie_set(transactions_trie, i, tx) receipt = self.make_receipt( diff --git a/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py new file mode 100644 index 0000000000..626edcddf0 --- /dev/null +++ b/src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py @@ -0,0 +1,256 @@ +""" +The module implements the raw EVM tracer for t8n. +""" +import json +import os +from dataclasses import dataclass, fields +from typing import List, Optional, Protocol, TextIO, Union, runtime_checkable + +from ethereum.base_types import U256, Bytes, Uint +from ethereum.trace import ( + EvmStop, + GasAndRefund, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TraceEvent, + TransactionEnd, + TransactionStart, +) + +EXCLUDE_FROM_OUTPUT = ["gasCostTraced", "errorTraced", "precompile"] + + +@dataclass +class Trace: + """ + The class implements the raw EVM trace. + """ + + pc: int + op: str + gas: str + gasCost: str + memSize: int + stack: List[str] + depth: int + refund: int + opName: str + gasCostTraced: bool = False + errorTraced: bool = False + precompile: bool = False + error: Optional[str] = None + + +@dataclass +class FinalTrace: + """ + The class implements final trace for a tx. + """ + + output: str + gasUsed: str + error: Optional[str] = None + + def __init__(self, gas_used: int, output: bytes, has_erred: bool) -> None: + self.output = output.hex() + self.gasUsed = hex(gas_used) + if has_erred: + self.error = "" + + +@runtime_checkable +class Environment(Protocol): + """ + The class implements the environment interface for trace. + """ + + traces: List[Union["Trace", "FinalTrace"]] + + +@runtime_checkable +class Message(Protocol): + """ + The class implements the message interface for trace. + """ + + depth: int + parent_evm: Optional["Evm"] + + +@runtime_checkable +class Evm(Protocol): + """ + The class implements the EVM interface for trace. + """ + + pc: Uint + stack: List[U256] + memory: bytearray + code: Bytes + gas_left: Uint + env: Environment + refund_counter: int + running: bool + message: Message + + +def evm_trace(evm: object, event: TraceEvent) -> None: + """ + Create a trace of the event. + """ + assert isinstance(evm, Evm) + last_trace: Optional[Union[Trace, FinalTrace]] + + refund_counter = evm.refund_counter + parent_evm = evm.message.parent_evm + while parent_evm is not None: + refund_counter += parent_evm.refund_counter + parent_evm = parent_evm.message.parent_evm + + if isinstance(event, TransactionStart): + pass + elif isinstance(event, TransactionEnd): + final_trace = FinalTrace(event.gas_used, event.output, event.has_erred) + evm.env.traces.append(final_trace) + elif isinstance(event, PrecompileStart): + new_trace = Trace( + pc=evm.pc, + op="0x" + event.address.hex().lstrip("0"), + gas=hex(evm.gas_left), + gasCost="0x0", + memSize=len(evm.memory), + stack=[hex(i) for i in evm.stack], + depth=evm.message.depth + 1, + refund=refund_counter, + opName="0x" + event.address.hex().lstrip("0"), + precompile=True, + ) + + evm.env.traces.append(new_trace) + elif isinstance(event, PrecompileEnd): + last_trace = evm.env.traces[-1] + assert isinstance(last_trace, Trace) + + last_trace.gasCostTraced = True + last_trace.errorTraced = True + elif isinstance(event, OpStart): + new_trace = Trace( + pc=evm.pc, + op=event.op.value, + gas=hex(evm.gas_left), + gasCost="0x0", + memSize=len(evm.memory), + stack=[hex(i) for i in evm.stack], + depth=evm.message.depth + 1, + refund=refund_counter, + opName=str(event.op).split(".")[-1], + ) + + evm.env.traces.append(new_trace) + elif isinstance(event, OpEnd): + last_trace = evm.env.traces[-1] + assert isinstance(last_trace, Trace) + + last_trace.gasCostTraced = True + last_trace.errorTraced = True + elif isinstance(event, OpException): + last_trace = None + if evm.env.traces: + last_trace = evm.env.traces[-1] + if last_trace is not None: + assert isinstance(last_trace, Trace) + if ( + # The first opcode in the code is an InvalidOpcode. + # So we add a new trace with InvalidOpcode as op. + not last_trace + # The current opcode is an InvalidOpcode. This condition + # is true if an InvalidOpcode is found in any location + # other than the first opcode. + or last_trace.errorTraced + # The first opcode in a child message is an InvalidOpcode. + # This case has to be explicitly handled since the first + # two conditions do not cover it. + or last_trace.depth == evm.message.depth + ): + new_trace = Trace( + pc=evm.pc, + op="InvalidOpcode", + gas=hex(evm.gas_left), + gasCost="0x0", + memSize=len(evm.memory), + stack=[hex(i) for i in evm.stack], + depth=evm.message.depth + 1, + refund=refund_counter, + opName="InvalidOpcode", + gasCostTraced=True, + errorTraced=True, + error="", + ) + + evm.env.traces.append(new_trace) + elif not last_trace.errorTraced: + # If the error for the last trace is not covered + # the exception is attributed to the last trace. + last_trace.error = "" + last_trace.errorTraced = True + elif isinstance(event, EvmStop): + if not evm.running: + return + elif len(evm.code) == 0: + return + else: + evm_trace(evm, OpStart(event.op)) + elif isinstance(event, GasAndRefund): + if not evm.env.traces: + # In contract creation transactions, there may not be any traces + return + + last_trace = evm.env.traces[-1] + assert isinstance(last_trace, Trace) + + if not last_trace.gasCostTraced: + last_trace.gasCost = hex(event.gas_cost) + last_trace.refund = refund_counter + last_trace.gasCostTraced = True + + +def output_op_trace( + trace: Union[Trace, FinalTrace], json_file: TextIO +) -> None: + """ + Output a single trace to a json file. + """ + dict_trace = { + field.name: getattr(trace, field.name) + for field in fields(trace) + if field.name not in EXCLUDE_FROM_OUTPUT + and getattr(trace, field.name) is not None + } + + json.dump(dict_trace, json_file, separators=(",", ":")) + json_file.write("\n") + + +def output_traces( + traces: List[Union[Trace, FinalTrace]], + tx_index: int, + tx_hash: bytes, + output_basedir: str = ".", +) -> None: + """ + Output the traces to a json file. + """ + tx_hash_str = "0x" + tx_hash.hex() + output_path = os.path.join( + output_basedir, f"trace-{tx_index}-{tx_hash_str}.jsonl" + ) + with open(output_path, "w") as json_file: + for trace in traces: + + if getattr(trace, "precompile", False): + # Traces related to pre-compile are not output. + continue + output_op_trace(trace, json_file) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py index 1c43c29d12..57520b46bd 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -210,16 +210,22 @@ def add_transaction(self, tx: Any) -> None: else: self.successful_txs.append(tx) - def add_receipt(self, tx: Any, gas_consumed: Uint) -> None: + def get_tx_hash(self, tx: Any) -> bytes: """ - Add t8n receipt info for valid tx + Get the transaction hash of a transaction. """ if self.t8n.is_after_fork("ethereum.berlin") and not isinstance( tx, self.t8n.fork_types.LegacyTransaction ): - tx_hash = keccak256(self.t8n.fork_types.encode_transaction(tx)) + return keccak256(self.t8n.fork_types.encode_transaction(tx)) else: - tx_hash = keccak256(rlp.encode(tx)) + return keccak256(rlp.encode(tx)) + + def add_receipt(self, tx: Any, gas_consumed: Uint) -> None: + """ + Add t8n receipt info for valid tx + """ + tx_hash = self.get_tx_hash(tx) data = { "transactionHash": "0x" + tx_hash.hex(), diff --git a/src/ethereum_spec_tools/evm_trace.py b/src/ethereum_spec_tools/evm_trace.py deleted file mode 100644 index 2a17357c4d..0000000000 --- a/src/ethereum_spec_tools/evm_trace.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -The module implements the raw EVM tracer for pytest -""" -import logging -from dataclasses import dataclass -from typing import Any, List - -from ethereum.base_types import U256, Bytes, Uint - - -@dataclass -class EvmTrace: - """ - The object that is logged when pytest log_cli_level - is set to 10. This can be used to create a raw trace of the evm. - - Contains the following: - - 1. `depth`: depth of the message - 2. `pc`: programcounter before opcode execution - 3. `op`: the opcode to be executed - 4. `has_erred`: has the evm erred before execution - 5. `start_gas`: evm.gas_left before execution starts - 6. `refund`: refund counter before opcode execution - 7. `output`: evm output - 8. `stack`: the stack before opcode execution - """ - - depth: Uint - pc: Uint - op: str - has_erred: bool - start_gas: U256 - refund: U256 - output: Bytes - stack: List[U256] - - def custom_repr(self) -> str: - """ - Add indentation for child evms (depth > 0) - """ - tabs = "\t" * self.depth - rep = tabs + self.__repr__() - return rep - - -def evm_trace(evm: Any, op: Any) -> None: - """ - Create a new trace instance before opcode execution - """ - if isinstance(op, bytes): - opcode = "0x" + op.hex().lstrip("0") - else: - opcode = str(op).split(".")[-1] - - new_trace = EvmTrace( - depth=evm.message.depth, - pc=evm.pc, - op=opcode, - has_erred=False, - start_gas=evm.gas_left, - refund=evm.refund_counter, - output=evm.output, - stack=evm.stack.copy(), - ) - logging.info(new_trace.custom_repr()) diff --git a/tests/berlin/test_state_transition.py b/tests/berlin/test_state_transition.py index a4564acd0e..600a40b989 100644 --- a/tests/berlin/test_state_transition.py +++ b/tests/berlin/test_state_transition.py @@ -178,6 +178,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: difficulty=genesis_block.header.difficulty, state=state, chain_id=Uint(1), + traces=[], ) with pytest.raises(InvalidBlock): diff --git a/tests/byzantium/test_state_transition.py b/tests/byzantium/test_state_transition.py index deb734c15a..e4753a068b 100644 --- a/tests/byzantium/test_state_transition.py +++ b/tests/byzantium/test_state_transition.py @@ -182,6 +182,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: time=genesis_block.header.timestamp, difficulty=genesis_block.header.difficulty, state=state, + traces=[], ) with pytest.raises(InvalidBlock): diff --git a/tests/conftest.py b/tests/conftest.py index 0f75e58f43..1010f4def5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,6 @@ from pytest import Session import ethereum -from ethereum_spec_tools.evm_trace import evm_trace from tests.helpers import TEST_FIXTURES @@ -20,14 +19,6 @@ def pytest_addoption(parser: Parser) -> None: """ Accept --evm-trace option in pytest. """ - parser.addoption( - "--evm-trace", - dest="vmtrace", - default=1, - action="store_const", - const=10, - help="Run trace", - ) parser.addoption( "--optimized", dest="optimized", @@ -42,10 +33,6 @@ def pytest_configure(config: Config) -> None: """ Configure the ethereum module and log levels to output evm trace. """ - if config.getoption("vmtrace", default=1) == 10: - config.option.__dict__["log_cli_level"] = "10" - config.option.__dict__["log_format"] = "%(message)s" - setattr(ethereum, "evm_trace", evm_trace) if config.getoption("optimized"): import ethereum_optimized diff --git a/tests/constantinople/test_state_transition.py b/tests/constantinople/test_state_transition.py index 87135f7c65..dead7793da 100644 --- a/tests/constantinople/test_state_transition.py +++ b/tests/constantinople/test_state_transition.py @@ -185,6 +185,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: time=genesis_block.header.timestamp, difficulty=genesis_block.header.difficulty, state=state, + traces=[], ) with pytest.raises(InvalidBlock): diff --git a/tests/frontier/test_state_transition.py b/tests/frontier/test_state_transition.py index 554b4c83d0..4da448a560 100644 --- a/tests/frontier/test_state_transition.py +++ b/tests/frontier/test_state_transition.py @@ -176,6 +176,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: time=genesis_block.header.timestamp, difficulty=genesis_block.header.difficulty, state=state, + traces=[], ) with pytest.raises(InvalidBlock): diff --git a/tests/helpers/load_vm_tests.py b/tests/helpers/load_vm_tests.py index 9bf0f44f69..9e45aa11be 100644 --- a/tests/helpers/load_vm_tests.py +++ b/tests/helpers/load_vm_tests.py @@ -151,6 +151,7 @@ def json_to_env(self, json_data: Any) -> Any: time=hex_to_u256(json_data["env"]["currentTimestamp"]), difficulty=hex_to_uint(json_data["env"]["currentDifficulty"]), state=current_state, + traces=[], ) def json_to_state(self, raw: Any) -> Any: diff --git a/tests/homestead/test_state_transition.py b/tests/homestead/test_state_transition.py index c06f2c244f..175b2bbde2 100644 --- a/tests/homestead/test_state_transition.py +++ b/tests/homestead/test_state_transition.py @@ -258,6 +258,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: time=genesis_block.header.timestamp, difficulty=genesis_block.header.difficulty, state=state, + traces=[], ) with pytest.raises(InvalidBlock): diff --git a/tests/istanbul/test_state_transition.py b/tests/istanbul/test_state_transition.py index 08406aedc8..f4114a6e22 100644 --- a/tests/istanbul/test_state_transition.py +++ b/tests/istanbul/test_state_transition.py @@ -179,6 +179,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: difficulty=genesis_block.header.difficulty, state=state, chain_id=Uint(1), + traces=[], ) with pytest.raises(InvalidBlock): diff --git a/tests/london/test_state_transition.py b/tests/london/test_state_transition.py index 19d733bcd2..928b41305b 100644 --- a/tests/london/test_state_transition.py +++ b/tests/london/test_state_transition.py @@ -182,6 +182,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: state=state, chain_id=Uint(1), base_fee_per_gas=Uint(16), + traces=[], ) with pytest.raises(InvalidBlock): diff --git a/tests/paris/test_state_transition.py b/tests/paris/test_state_transition.py index e15c30e50c..771597a3d7 100644 --- a/tests/paris/test_state_transition.py +++ b/tests/paris/test_state_transition.py @@ -182,6 +182,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: state=state, chain_id=Uint(1), base_fee_per_gas=Uint(16), + traces=[], ) with pytest.raises(InvalidBlock): diff --git a/tests/spurious_dragon/test_state_transition.py b/tests/spurious_dragon/test_state_transition.py index 4f780688d4..177ab5a829 100644 --- a/tests/spurious_dragon/test_state_transition.py +++ b/tests/spurious_dragon/test_state_transition.py @@ -176,6 +176,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: time=genesis_block.header.timestamp, difficulty=genesis_block.header.difficulty, state=state, + traces=[], ) with pytest.raises(InvalidBlock): diff --git a/tests/tangerine_whistle/test_state_transition.py b/tests/tangerine_whistle/test_state_transition.py index 5d2c007271..57b7062332 100644 --- a/tests/tangerine_whistle/test_state_transition.py +++ b/tests/tangerine_whistle/test_state_transition.py @@ -178,6 +178,7 @@ def test_transaction_with_insufficient_balance_for_value() -> None: time=genesis_block.header.timestamp, difficulty=genesis_block.header.difficulty, state=state, + traces=[], ) with pytest.raises(InvalidBlock): diff --git a/whitelist.txt b/whitelist.txt index c30dbcab89..927fa8c0e4 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -386,4 +386,7 @@ basedir tf casefold rlps -jsons \ No newline at end of file +jsons +mem + +checkable \ No newline at end of file