From 843314f198e11108ba48936a451691076acc1332 Mon Sep 17 00:00:00 2001 From: robert Date: Sat, 24 Nov 2018 10:02:54 -0800 Subject: [PATCH 1/2] [WIP] debug_traceTransaction endpoint implementation --- eth/vm/base.py | 40 ++-- eth/vm/computation.py | 99 +++++---- eth/vm/forks/frontier/computation.py | 1 + eth/vm/forks/frontier/state.py | 24 ++- eth/vm/stack.py | 25 ++- eth/vm/state.py | 23 ++- eth/vm/tracing.py | 216 ++++++++++++++++++++ tests/core/opcodes/test_opcodes.py | 6 +- tests/core/vm/test_base_computation.py | 2 + tests/core/vm/test_frontier_computation.py | 6 +- tests/core/vm/test_tracing.py | 111 ++++++++++ tests/json-fixtures/test_virtual_machine.py | 5 + 12 files changed, 490 insertions(+), 68 deletions(-) create mode 100644 eth/vm/tracing.py create mode 100644 tests/core/vm/test_tracing.py diff --git a/eth/vm/base.py b/eth/vm/base.py index fea39e01d6..817a7cc342 100644 --- a/eth/vm/base.py +++ b/eth/vm/base.py @@ -74,11 +74,15 @@ validate_length_lte, validate_gas_limit, ) +from eth.vm.computation import BaseComputation from eth.vm.message import ( Message, ) from eth.vm.state import BaseState -from eth.vm.computation import BaseComputation +from eth.vm.tracing import ( + BaseTracer, + NoopTracer, +) class BaseVM(Configurable, ABC): @@ -111,8 +115,9 @@ def logger(self) -> logging.Logger: @abstractmethod def apply_transaction(self, header: BlockHeader, - transaction: BaseTransaction - ) -> Tuple[BlockHeader, Receipt, BaseComputation]: + transaction: BaseTransaction, + *, + tracer: BaseTracer=None) -> Tuple[BlockHeader, Receipt, BaseComputation]: raise NotImplementedError("VM classes must implement this method") @abstractmethod @@ -397,8 +402,9 @@ def logger(self) -> logging.Logger: # def apply_transaction(self, header: BlockHeader, - transaction: BaseTransaction - ) -> Tuple[BlockHeader, Receipt, BaseComputation]: + transaction: BaseTransaction, + *, + tracer: BaseTracer=None) -> Tuple[BlockHeader, Receipt, BaseComputation]: """ Apply the transaction to the current block. This is a wrapper around :func:`~eth.vm.state.State.apply_transaction` with some extra orchestration logic. @@ -406,8 +412,12 @@ def apply_transaction(self, :param header: header of the block before application :param transaction: to apply """ + if tracer is None: + tracer = NoopTracer() + self.validate_transaction_against_header(header, transaction) - state_root, computation = self.state.apply_transaction(transaction) + with self.state.trace(tracer): + state_root, computation = self.state.apply_transaction(transaction) receipt = self.make_receipt(header, transaction, computation, self.state) self.validate_receipt(receipt) @@ -429,7 +439,8 @@ def execute_bytecode(self, data: bytes, code: bytes, code_address: Address=None, - ) -> BaseComputation: + *, + tracer: BaseTracer=None) -> BaseComputation: """ Execute raw bytecode in the context of the current state of the virtual machine. @@ -437,6 +448,9 @@ def execute_bytecode(self, if origin is None: origin = sender + if tracer is None: + tracer = NoopTracer() + # Construct a message message = Message( gas=gas, @@ -455,11 +469,13 @@ def execute_bytecode(self, ) # Execute it in the VM - return self.state.get_computation(message, transaction_context).apply_computation( - self.state, - message, - transaction_context, - ) + with self.state.trace(tracer): + return self.state.get_computation(message, transaction_context).apply_computation( + self.state, + message, + transaction_context, + tracer, + ) def apply_all_transactions( self, diff --git a/eth/vm/computation.py b/eth/vm/computation.py index a6ca25a3da..2b9b4fd9b5 100644 --- a/eth/vm/computation.py +++ b/eth/vm/computation.py @@ -1,6 +1,6 @@ from abc import ( ABC, - abstractmethod + abstractmethod, ) import itertools import logging @@ -10,6 +10,7 @@ cast, Dict, List, + Optional, Tuple, Union, ) @@ -20,7 +21,6 @@ from eth_utils import ( encode_hex, ) - from eth.constants import ( GAS_MEMORY, GAS_MEMORY_QUADRATIC_DENOMINATOR, @@ -46,33 +46,18 @@ validate_is_bytes, validate_uint256, ) -from eth.vm.code_stream import ( - CodeStream, -) -from eth.vm.gas_meter import ( - GasMeter, -) -from eth.vm.logic.invalid import ( - InvalidOpcode, -) -from eth.vm.memory import ( - Memory, -) -from eth.vm.message import ( - Message, -) -from eth.vm.opcode import ( # noqa: F401 - Opcode -) -from eth.vm.stack import ( - Stack, -) -from eth.vm.state import ( - BaseState, -) -from eth.vm.transaction_context import ( - BaseTransactionContext +from eth.vm.code_stream import CodeStream +from eth.vm.gas_meter import GasMeter +from eth.vm.logic.invalid import InvalidOpcode +from eth.vm.memory import Memory +from eth.vm.message import Message +from eth.vm.opcode import Opcode +from eth.vm.stack import Stack +from eth.vm.state import BaseState +from eth.vm.tracing import ( + BaseTracer, ) +from eth.vm.transaction_context import BaseTransactionContext def memory_gas_cost(size_in_bytes: int) -> int: @@ -125,7 +110,8 @@ class BaseComputation(Configurable, ABC): def __init__(self, state: BaseState, message: Message, - transaction_context: BaseTransactionContext) -> None: + transaction_context: BaseTransactionContext, + tracer: BaseTracer) -> None: self.state = state self.msg = message @@ -142,6 +128,8 @@ def __init__(self, code = message.code self.code = CodeStream(code) + self.tracer = tracer + # # Convenience # @@ -152,9 +140,31 @@ def is_origin_computation(self) -> bool: """ return self.msg.sender == self.transaction_context.origin + # + # Program Counter + # + def get_pc(self) -> int: + return max(0, self.code.pc - 1) + # # Error handling # + @property + def error(self) -> Optional[VMError]: + if self.is_error: + return self._error + else: + return None + + @error.setter + def error(self, value: VMError) -> None: + if not isinstance(value, VMError): + raise TypeError( + "Computation.error can only be set to a VMError subclass. Got: " + "{0}".format(value) + ) + self._error = value + @property def is_success(self) -> bool: """ @@ -175,15 +185,15 @@ def raise_if_error(self) -> None: :raise VMError: """ - if self._error is not None: - raise self._error + if self.is_error: + raise self.error @property def should_burn_gas(self) -> bool: """ Return ``True`` if the remaining gas should be burned. """ - return self.is_error and self._error.burns_gas + return self.is_error and self.error.burns_gas @property def should_return_gas(self) -> bool: @@ -258,6 +268,9 @@ def memory_read_bytes(self, start_position: int, size: int) -> bytes: """ return self._memory.read_bytes(start_position, size) + def dump_memory(self) -> bytes: + return self._memory.read_bytes(0, len(self._memory)) + # # Gas Consumption # @@ -341,6 +354,9 @@ def stack_dup(self, position: int) -> None: """ return self._stack.dup(position) + def dump_stack(self) -> Tuple[int, ...]: + return tuple(self._stack) # type: ignore + # # Computation result # @@ -402,12 +418,14 @@ def generate_child_computation(self, child_msg: Message) -> 'BaseComputation': self.state, child_msg, self.transaction_context, + self.tracer, ).apply_create_message() else: child_computation = self.__class__( self.state, child_msg, self.transaction_context, + self.tracer, ).apply_message() return child_computation @@ -511,7 +529,7 @@ def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None: "y" if self.msg.is_static else "n", exc_value, ) - self._error = exc_value + self.error = exc_value if self.should_burn_gas: self.consume_gas( self._gas_meter.gas_remaining, @@ -559,11 +577,12 @@ def apply_create_message(self) -> 'BaseComputation': def apply_computation(cls, state: BaseState, message: Message, - transaction_context: BaseTransactionContext) -> 'BaseComputation': + transaction_context: BaseTransactionContext, + tracer: BaseTracer) -> 'BaseComputation': """ Perform the computation that would be triggered by the VM message. """ - with cls(state, message, transaction_context) as computation: + with cls(state, message, transaction_context, tracer) as computation: # Early exit on pre-compiles if message.code_address in computation.precompiles: computation.precompiles[message.code_address](computation) @@ -571,18 +590,24 @@ def apply_computation(cls, for opcode in computation.code: opcode_fn = computation.get_opcode_fn(opcode) + pc = computation.get_pc() computation.logger.debug2( "OPCODE: 0x%x (%s) | pc: %s", opcode, opcode_fn.mnemonic, - max(0, computation.code.pc - 1), + pc, ) try: - opcode_fn(computation=computation) + with computation.tracer.capture(computation, opcode_fn): + opcode_fn(computation=computation) except Halt: break + + # Allow the tracer to record final values. + computation.tracer.finalize(computation) + return computation # diff --git a/eth/vm/forks/frontier/computation.py b/eth/vm/forks/frontier/computation.py index 25f175d8bf..5fb98c8387 100644 --- a/eth/vm/forks/frontier/computation.py +++ b/eth/vm/forks/frontier/computation.py @@ -74,6 +74,7 @@ def apply_message(self) -> BaseComputation: self.state, self.msg, self.transaction_context, + self.tracer, ) if computation.is_error: diff --git a/eth/vm/forks/frontier/state.py b/eth/vm/forks/frontier/state.py index bd637f7f5d..025a53ad45 100644 --- a/eth/vm/forks/frontier/state.py +++ b/eth/vm/forks/frontier/state.py @@ -31,6 +31,10 @@ BaseState, BaseTransactionExecutor, ) +from eth.vm.tracing import ( + BaseTracer, + NoopTracer, +) from .computation import FrontierComputation @@ -106,8 +110,12 @@ def build_evm_message(self, transaction: BaseOrSpoofTransaction) -> Message: def build_computation(self, message: Message, - transaction: BaseOrSpoofTransaction) -> BaseComputation: + transaction: BaseOrSpoofTransaction, + tracer: BaseTracer=None) -> BaseComputation: """Apply the message to the VM.""" + if tracer is None: + tracer = NoopTracer() + transaction_context = self.vm_state.get_transaction_context(transaction) if message.is_create: is_collision = self.vm_state.account_db.account_has_code_or_nonce( @@ -128,14 +136,16 @@ def build_computation(self, encode_hex(message.storage_address), ) else: + with self.vm_state.trace(tracer): + computation = self.vm_state.get_computation( + message, + transaction_context, + ).apply_create_message() + else: + with self.vm_state.trace(tracer): computation = self.vm_state.get_computation( message, - transaction_context, - ).apply_create_message() - else: - computation = self.vm_state.get_computation( - message, - transaction_context).apply_message() + transaction_context).apply_message() return computation diff --git a/eth/vm/stack.py b/eth/vm/stack.py index 2c9028b205..e79f600cc6 100644 --- a/eth/vm/stack.py +++ b/eth/vm/stack.py @@ -1,4 +1,11 @@ import logging +from typing import ( # noqa: F401 + Generator, + Iterable, + List, + Tuple, + Union +) from eth_utils import ( int_to_big_endian, @@ -13,12 +20,6 @@ validate_stack_item, ) -from typing import ( # noqa: F401 - Generator, - List, - Tuple, - Union -) from eth_typing import Hash32 # noqa: F401 @@ -35,6 +36,18 @@ def __init__(self) -> None: def __len__(self) -> int: return len(self.values) + def __iter__(self) -> Iterable[int]: + for item in self.values: + if isinstance(item, int): + yield item + elif isinstance(item, bytes): + yield big_endian_to_int(item) + else: + raise TypeError( + "Invariant: stack should only contain `int` and `bytes` " + "types. Got: {!r}".format(item) + ) + def push(self, value: Union[int, bytes]) -> None: """ Push an item onto the stack. diff --git a/eth/vm/state.py b/eth/vm/state.py index 160a4defd0..1d65ff6d08 100644 --- a/eth/vm/state.py +++ b/eth/vm/state.py @@ -8,6 +8,7 @@ cast, Callable, Iterator, + Optional, Tuple, Type, TYPE_CHECKING, @@ -45,6 +46,7 @@ ExecutionContext, ) from eth.vm.message import Message +from eth.vm.tracing import BaseTracer if TYPE_CHECKING: from eth.computation import ( # noqa: F401 @@ -85,6 +87,8 @@ class for vm execution. account_db_class = None # type: Type[BaseAccountDB] transaction_executor = None # type: Type[BaseTransactionExecutor] + tracer = None # type: BaseTracer + def __init__(self, db: BaseDB, execution_context: ExecutionContext, state_root: bytes) -> None: self._db = db self.execution_context = execution_context @@ -99,9 +103,19 @@ def logger(self) -> ExtendedDebugLogger: return cast(ExtendedDebugLogger, normal_logger) # - # Block Object Properties (in opcodes) + # Tracing # + @contextlib.contextmanager + def trace(self, tracer: BaseTracer) -> Iterator[None]: + self.tracer = tracer + try: + yield + finally: + self.tracer = None + # + # Block Object Properties (in opcodes) + # @property def coinbase(self) -> Address: """ @@ -220,7 +234,7 @@ def get_computation(self, if self.computation_class is None: raise AttributeError("No `computation_class` has been set for this State") else: - computation = self.computation_class(self, message, transaction_context) + computation = self.computation_class(self, message, transaction_context, self.tracer) return computation # @@ -299,7 +313,7 @@ def __init__(self, vm_state: BaseState) -> None: def __call__(self, transaction: BaseOrSpoofTransaction) -> 'BaseComputation': valid_transaction = self.validate_transaction(transaction) message = self.build_evm_message(valid_transaction) - computation = self.build_computation(message, valid_transaction) + computation = self.build_computation(message, valid_transaction, self.vm_state.tracer) finalized_computation = self.finalize_computation(valid_transaction, computation) return finalized_computation @@ -314,7 +328,8 @@ def build_evm_message(self, transaction: BaseOrSpoofTransaction) -> Message: @abstractmethod def build_computation(self, message: Message, - transaction: BaseOrSpoofTransaction) -> 'BaseComputation': + transaction: BaseOrSpoofTransaction, + tracer: BaseTracer) -> 'BaseComputation': raise NotImplementedError() @abstractmethod diff --git a/eth/vm/tracing.py b/eth/vm/tracing.py new file mode 100644 index 0000000000..273367c7e4 --- /dev/null +++ b/eth/vm/tracing.py @@ -0,0 +1,216 @@ +from abc import ( + ABC, + abstractmethod, +) + +import contextlib + +import logging + +from typing import ( + Dict, + Iterator, + NamedTuple, + Optional, + Tuple, + TYPE_CHECKING, +) + +from eth_typing import ( + Address, +) + +from eth_utils import ValidationError + +from eth.exceptions import VMError +from eth.vm.opcode import Opcode + +if TYPE_CHECKING: + from typing import List # noqa: F401 + from eth.vm.computation import BaseComputation # noqa: F401 + + +class BaseTracer(ABC): + @contextlib.contextmanager + @abstractmethod + def capture(self, computation: 'BaseComputation', opcode_fn: Opcode) -> None: + pass + + @abstractmethod + def finalize(self, computation: 'BaseComputation') -> None: + pass + + +class NoopTracer(BaseTracer): + """ + A Tracer class which does nothing. + """ + + @contextlib.contextmanager + def capture(self, computation: 'BaseComputation', opcode_fn: Opcode) -> Iterator[None]: + yield + + def finalize(self, computation: 'BaseComputation') -> None: + pass + + +class Storage(object): + __slots__ = ['store'] + + def __init__(self) -> None: + self.store = {} # type: Dict[Address, Dict[int, int]] + + def dump(self, address: Address) -> Dict[int, int]: + if address not in self.store: + return {} + return self.store[address] + + def set_slot(self, address: Address, slot: int, value: int) -> None: + if address not in self.store: + self.store[address] = {} + self.store[address][slot] = value + + +StructLogEntry = NamedTuple('StructLogEntry', + [ + ('depth', int), + ('err', VMError), + ('gas', int), + ('gas_cost', int), + ('memory', Optional[bytes]), + ('op', str), + ('pc', int), + ('stack', Optional[Tuple[int, ...]]), + ('storage', Optional[Dict[int, int]]) + ]) + +ExecutionResult = NamedTuple('ExecutionResult', + [ + ('error', bool), + ('gas', int), + ('output', bytes), + ('logs', Tuple[StructLogEntry, ...]), + ]) + + +class StructTracer(BaseTracer): + """ + A Tracer class which implements structured log tracing: + + https://github.com/ethereum/go-ethereum/wiki/Tracing:-Introduction + """ + logger = logging.getLogger('eth.vm.tracing.StructTracer') + + result = None + + def __init__(self, + memory: bool = True, + stack: bool = True, + storage: bool = True, + limit: int = None): + self.changed_values = Storage() + self.is_memory_enabled = memory + self.is_stack_enabled = stack + self.is_storage_enabled = storage + self.limit = limit + self.logs = [] # type: List[StructLogEntry] + + @property + def is_full(self) -> bool: + if self.limit is None: + return False + else: + return len(self.logs) >= self.limit + + @property + def is_final(self) -> bool: + return self.result is not None + + @contextlib.contextmanager + def capture(self, computation: 'BaseComputation', opcode_fn: Opcode) -> Iterator[None]: + if self.is_final: + raise ValidationError("Cannot capture using a finalized tracer") + + pc = computation.get_pc() + start_gas = computation.get_gas_remaining() + stack = computation.dump_stack() if self.is_storage_enabled else None + memory = computation.dump_memory() if self.is_memory_enabled else None + + if self.is_storage_enabled: + storage_address = computation.msg.storage_address + if opcode_fn.mnemonic == "SSTORE" and len(stack) >= 2: + val = stack[-2] + slot = stack[-1] + self.changed_values.set_slot(storage_address, slot, val) + storage = self.changed_values.dump(storage_address) + else: + storage = None + + try: + yield + except VMError as err: + self._log_operation( + depth=computation.msg.depth + 1, + error=err, + gas=start_gas, + gas_cost=start_gas - computation.get_gas_remaining(), + memory=memory, + op=opcode_fn.mnemonic, + pc=pc, + stack=stack, + storage=storage + ) + raise + else: + self._log_operation( + depth=computation.msg.depth + 1, + error=None, + gas=start_gas, + gas_cost=start_gas - computation.get_gas_remaining(), + memory=memory, + op=opcode_fn.mnemonic, + pc=pc, + stack=stack, + storage=storage + ) + + def finalize(self, computation: 'BaseComputation') -> None: + if self.is_final: + raise ValidationError("Cannot finalize tracer which is already finalized") + elif computation.is_origin_computation: + self.result = ExecutionResult( + error=computation.error is not None, + gas=computation.get_gas_used(), + logs=tuple(self.logs), + output=computation.output, + ) + + def _log_operation(self, + *, + depth: int, + error: Optional[VMError], + gas: int, + gas_cost: int, + memory: bytes, + op: str, + pc: int, + stack: Tuple[int, ...], + storage: Dict[int, int]) -> None: + if self.is_full: + self.logger.debug( + 'StructTracer full (limit=%d). Discarding trace log entry', + self.limit, + ) + return + + self.logs.append(StructLogEntry( + depth=depth, + err=error, + gas=gas, + gas_cost=gas_cost, + memory=memory, + op=op, + pc=pc, + stack=stack, + storage=storage, + )) diff --git a/tests/core/opcodes/test_opcodes.py b/tests/core/opcodes/test_opcodes.py index e3717918b0..4d54c20663 100644 --- a/tests/core/opcodes/test_opcodes.py +++ b/tests/core/opcodes/test_opcodes.py @@ -35,6 +35,9 @@ from eth.vm.message import ( Message, ) +from eth.vm.tracing import ( + NoopTracer, +) NORMALIZED_ADDRESS_A = "0x0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6" @@ -51,7 +54,7 @@ ) -def setup_computation(vm_class, create_address, code): +def setup_computation(vm_class, create_address, code, tracer=NoopTracer()): message = Message( to=CANONICAL_ADDRESS_A, @@ -74,6 +77,7 @@ def setup_computation(vm_class, create_address, code): state=vm.state, message=message, transaction_context=tx_context, + tracer=tracer, ) return computation diff --git a/tests/core/vm/test_base_computation.py b/tests/core/vm/test_base_computation.py index 7421662497..9105d341c8 100644 --- a/tests/core/vm/test_base_computation.py +++ b/tests/core/vm/test_base_computation.py @@ -67,6 +67,7 @@ def computation(message, transaction_context): state=None, message=message, transaction_context=transaction_context, + tracer=None, ) return computation @@ -98,6 +99,7 @@ def test_is_origin_computation(computation, transaction_context): state=None, message=message2, transaction_context=transaction_context, + tracer=None, ) assert not computation2.is_origin_computation diff --git a/tests/core/vm/test_frontier_computation.py b/tests/core/vm/test_frontier_computation.py index b96442ecd0..125c06330b 100644 --- a/tests/core/vm/test_frontier_computation.py +++ b/tests/core/vm/test_frontier_computation.py @@ -13,6 +13,9 @@ from eth.vm.transaction_context import ( BaseTransactionContext, ) +from eth.vm.tracing import ( + NoopTracer, +) NORMALIZED_ADDRESS_A = "0x0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6" @@ -51,11 +54,12 @@ def message(): @pytest.fixture -def computation(message, transaction_context, state): +def computation(message, transaction_context, state, tracer=NoopTracer()): computation = FrontierComputation( state=state, message=message, transaction_context=transaction_context, + tracer=tracer ) return computation diff --git a/tests/core/vm/test_tracing.py b/tests/core/vm/test_tracing.py new file mode 100644 index 0000000000..0433988816 --- /dev/null +++ b/tests/core/vm/test_tracing.py @@ -0,0 +1,111 @@ + +import pytest + +from eth_utils import decode_hex, to_int + +from eth.tools.builder.chain import api as b +from eth.vm.tracing import StructTracer +from eth.chains.base import Chain + + +CONTRACT_ADDRESS = decode_hex('0x1000000000000000000000000000000000000000') +# From `fixtures/VMTests/vmArithmetic/add0.json' +CONTRACT_CODE = decode_hex('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7ff' + 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01600055' + ) + + +@pytest.fixture(params=b.mainnet_fork_at_fns) +def chain(request, funded_address, funded_address_initial_balance): + fork_at_fn = request.param + return b.build( + Chain, + fork_at_fn(0), + b.disable_pow_check(), + b.genesis( + params={'gas_limit': 3141592}, + state=( + (funded_address, 'balance', funded_address_initial_balance), + (CONTRACT_ADDRESS, 'code', CONTRACT_CODE), + ), + ) + ) + + +def mk_transaction( + vm, + private_key, + to, + amount=0, + gas_price=1, + gas=100000, + data=b''): + """ + Create and return a transaction sending amount from to . + + The transaction will be signed with the given private key. + """ + nonce = vm.state.account_db.get_nonce(private_key.public_key.to_canonical_address()) + tx = vm.create_unsigned_transaction( + nonce=nonce, + gas_price=gas_price, + gas=gas, + to=to, + value=amount, + data=data, + ) + return tx.as_signed_transaction(private_key) + + +def test_trace_simple_value_transfer( + chain, + funded_address_private_key): + vm = chain.get_vm() + recipient = decode_hex('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0c') + amount = 100 + tx = mk_transaction(vm, funded_address_private_key, recipient, amount) + tracer = StructTracer() + _, _, computation = vm.apply_transaction(vm.block.header, tx, tracer=tracer) + + result = tracer.result + + assert result.error is (computation.error is not None) + assert computation.output == result.output + assert result.gas == computation.get_gas_used() + assert len(result.logs) == 0 + + +def test_trace_add0( + chain, + funded_address_private_key): + vm = chain.get_vm() + recipient = CONTRACT_ADDRESS + tx = mk_transaction(vm, funded_address_private_key, recipient) + tracer = StructTracer() + _, _, computation = vm.apply_transaction(vm.block.header, tx, tracer=tracer) + + result = tracer.result + + assert result.error is False + assert computation.output == b'' + assert result.gas == computation.get_gas_used() + + # Ensure that the actual opcodes are accurate + expected_ops = ['PUSH32', 'PUSH32', 'ADD', 'PUSH1', 'SSTORE'] + actual_ops = [entry.op for entry in result.logs] + assert actual_ops == expected_ops + + log_0, log_1, log_2, log_3, log_4 = result.logs + expected_add_result = to_int(hexstr='0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + 'fffffffe') + + # Check expected stack size + assert len(log_0.stack) == 0 + assert len(log_1.stack) == 1 # (left operand) + assert len(log_2.stack) == 2 # (left operand, right operand) + assert len(log_3.stack) == 1 # (add result) + assert len(log_4.stack) == 2 # (add result, storage_slot) + + last_storage = result.logs[-1].storage + assert 0 in last_storage + assert last_storage[0] == expected_add_result diff --git a/tests/json-fixtures/test_virtual_machine.py b/tests/json-fixtures/test_virtual_machine.py index 906140b7d6..0c0c48634c 100644 --- a/tests/json-fixtures/test_virtual_machine.py +++ b/tests/json-fixtures/test_virtual_machine.py @@ -45,6 +45,9 @@ from eth.vm.transaction_context import ( BaseTransactionContext, ) +from eth.vm.tracing import ( + NoopTracer, +) ROOT_PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) @@ -157,6 +160,7 @@ def fixture_to_computation(fixture, code, vm): vm.state, message, transaction_context, + NoopTracer() ) @@ -212,6 +216,7 @@ def test_vm_fixtures(fixture, vm_class, computation_getter): vm.state, message, transaction_context, + NoopTracer() ) # Update state_root manually vm.block = vm.block.copy( From 1bcbdca490f74423612156d458e8a4821954789b Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Wed, 9 Jan 2019 12:05:34 -0700 Subject: [PATCH 2/2] eliminate use of classmethod on instance of computation --- eth/vm/base.py | 2 +- eth/vm/state.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/eth/vm/base.py b/eth/vm/base.py index 817a7cc342..0831d314da 100644 --- a/eth/vm/base.py +++ b/eth/vm/base.py @@ -470,7 +470,7 @@ def execute_bytecode(self, # Execute it in the VM with self.state.trace(tracer): - return self.state.get_computation(message, transaction_context).apply_computation( + return self.state.get_computation_class().apply_computation( self.state, message, transaction_context, diff --git a/eth/vm/state.py b/eth/vm/state.py index 1d65ff6d08..20f1f2f2d7 100644 --- a/eth/vm/state.py +++ b/eth/vm/state.py @@ -225,17 +225,20 @@ def get_ancestor_hash(self, block_number: int) -> Hash32: # # Computation # + @classmethod + def get_computation_class(cls) -> 'BaseComputation': + if cls.computation_class is None: + raise AttributeError("No `computation_class` has been set for this State") + else: + return cls.computation_class + def get_computation(self, message: Message, transaction_context: 'BaseTransactionContext') -> 'BaseComputation': """ Return a computation instance for the given `message` and `transaction_context` """ - if self.computation_class is None: - raise AttributeError("No `computation_class` has been set for this State") - else: - computation = self.computation_class(self, message, transaction_context, self.tracer) - return computation + return self.get_computation_class()(self, message, transaction_context, self.tracer) # # Transaction context