ethereum / eth-tester

Tool suite for testing ethereum applications.
MIT License
359 stars 141 forks source link

Feature: Configurable transaction failure handler #133

Open fubuloubu opened 6 years ago

fubuloubu commented 6 years ago

Currently, when a transaction in eth-tester throws an exception (at least in pytest), it provides the entire backtrace of the failure, which is mostly showing the layers of calls back to py-evm, most of which is sort of meaningless to the user.

It would be more helpful to a user of eth-tester if there was a handler for this behavior that could given the execution trace or something so tools like @jacqueswww's vdb could handle this exception, or more generally a better trace could be produced (the EVM stack trace instead of the Python trace).

If you make this configurable, different compilers could provide language-specific behavior from giving the source code mapping, which eth-tester could use to retrace the exact line in the source program that caused the issue. This would be MUCH more helpful to debug. The default could just be an EVM opcode printer that shows the raw stack trace.

This is my thinking from sort of a high level user what they would want to see. May make more sense in the pytest-ethereum tool, but I could see this be more broadly useful and enable a general-purpose debugging API for other tools to leverage.

fubuloubu commented 6 years ago

Currently, this is what my trace looks like when there is a reversion with pytest:

args = (<eth_tester.backends.pyevm.main.PyEVMBackend object at 0x7f923a015748>, {'data': b'\xf9\x9e\xbe[\x00\x00\x00\x00\x00\...om': b'~_ER\t\x1ai\x12]]\xfc\xb7\xb8\xc2e\x90)9[\xdf', 'to': b')F%\x9e\x034\xf3:\x06A\x060$\x15\xad3\x91\xbe\xd3\x84'})
kwargs = {}

    @functools.wraps(to_wrap)
    def wrapper(*args, **kwargs):
        try:
>           return to_wrap(*args, **kwargs)

/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/eth_tester/utils/formatting.py:85: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <eth_tester.backends.pyevm.main.PyEVMBackend object at 0x7f923a015748>
transaction = {'data': b'\xf9\x9e\xbe[\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00~_ER\t\x1ai\x12]]\xfc\xb7\xb8\xc2e\x90)9[\xdf\...rom': b'~_ER\t\x1ai\x12]]\xfc\xb7\xb8\xc2e\x90)9[\xdf', 'to': b')F%\x9e\x034\xf3:\x06A\x060$\x15\xad3\x91\xbe\xd3\x84'}

    @replace_exceptions({
        EVMInvalidInstruction: TransactionFailed,
        EVMRevert: TransactionFailed})
    def estimate_gas(self, transaction):
        evm_transaction = self._get_normalized_and_unsigned_evm_transaction(assoc(
            transaction, 'gas', 21000))
        spoofed_transaction = EVMSpoofTransaction(evm_transaction, from_=transaction['from'])

>       return self.chain.estimate_gas(spoofed_transaction)

/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/eth_tester/backends/pyevm/main.py:475: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <eth_tester.backends.pyevm.main.setup_tester_chain.<locals>.MainnetTesterNoProofChain object at 0x7f9234393630>, transaction = <eth.utils.spoof.SpoofTransaction object at 0x7f92343ddc18>
at_header = <BlockHeader #2 dd710aa9>

    def estimate_gas(
            self,
            transaction: Union[BaseTransaction, SpoofTransaction],
            at_header: BlockHeader=None) -> int:
        """
            Returns an estimation of the amount of gas the given transaction will
            use if executed on top of the block specified by the given header.
            """
        if at_header is None:
            at_header = self.get_canonical_head()
        with self.get_vm(at_header).state_in_temp_block() as state:
>           return self.gas_estimator(state, transaction)

/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/eth/chains/base.py:607: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

>   ???

cytoolz/functoolz.pyx:232: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

state = <eth.vm.forks.byzantium.state.ByzantiumState object at 0x7f9234134278>, transaction = <eth.utils.spoof.SpoofTransaction object at 0x7f92343ddc18>, tolerance = 21000

    @curry
    def binary_gas_search(state, transaction, tolerance=1):
        """
        Run the transaction with various gas limits, progressively
        approaching the minimum needed to succeed without an OutOfGas exception.

        The starting range of possible estimates is:
        [transaction.intrinsic_gas, state.gas_limit].
        After the first OutOfGas exception, the range is: (largest_limit_out_of_gas, state.gas_limit].
        After the first run not out of gas, the range is: (largest_limit_out_of_gas, smallest_success].

        :param int tolerance: When the range of estimates is less than tolerance,
            return the top of the range.
        :returns int: The smallest confirmed gas to not throw an OutOfGas exception,
            subject to tolerance. If OutOfGas is thrown at block limit, return block limit.
        :raises VMError: if the computation fails even when given the block gas_limit to complete
        """
        if not hasattr(transaction, 'sender'):
            raise TypeError(
                "Transaction is missing attribute sender.",
                "If sending an unsigned transaction, use SpoofTransaction and provide the",
                "sender using the 'from' parameter")

        minimum_transaction = SpoofTransaction(
            transaction,
            gas=transaction.intrinsic_gas,
            gas_price=0,
        )

        if _get_computation_error(state, minimum_transaction) is None:
            return transaction.intrinsic_gas

        maximum_transaction = SpoofTransaction(
            transaction,
            gas=state.gas_limit,
            gas_price=0,
        )
        error = _get_computation_error(state, maximum_transaction)
        if error is not None:
>           raise error

/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/eth/estimators/gas.py:65: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

cls = <class 'eth.vm.forks.byzantium.computation.ByzantiumComputation'>, state = <eth.vm.forks.byzantium.state.ByzantiumState object at 0x7f9234134278>
message = <eth.vm.message.Message object at 0x7f92343a7a60>, transaction_context = <eth.vm.forks.frontier.transaction_context.FrontierTransactionContext object at 0x7f9234134868>

    @classmethod
    def apply_computation(cls,
                          state: BaseState,
                          message: Message,
                          transaction_context: BaseTransactionContext) -> 'BaseComputation':
        """
            Perform the computation that would be triggered by the VM message.
            """
        with cls(state, message, transaction_context) as computation:
            # Early exit on pre-compiles
            if message.code_address in computation.precompiles:
                computation.precompiles[message.code_address](computation)
                return computation

            for opcode in computation.code:
                opcode_fn = computation.get_opcode_fn(opcode)

                computation.logger.trace(
                    "OPCODE: 0x%x (%s) | pc: %s",
                    opcode,
                    opcode_fn.mnemonic,
                    max(0, computation.code.pc - 1),
                )

                try:
>                   opcode_fn(computation=computation)

/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/eth/vm/computation.py:560: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

computation = <eth.vm.forks.byzantium.computation.ByzantiumComputation object at 0x7f92342a3a58>

    def revert(computation: BaseComputation) -> None:
        start_position, size = computation.stack_pop(num_items=2, type_hint=constants.UINT256)

        computation.extend_memory(start_position, size)

        output = computation.memory_read(start_position, size)
        computation.output = bytes(output)
>       raise Revert(computation.output)
E       eth.exceptions.Revert: b''

/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/eth/vm/logic/system.py:48: Revert

The above exception was the direct cause of the following exception:

accounts = ['0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf', '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF', '0x6813Eb9362372EEF6200f3...0B5d10E37751FE6AA718', '0xe1AB8145F7E55DC933d51a18c793F901A3A0b276', '0xE57bFE9F44b819898F47BF37E5AF72a0783e1141', ...]
contract = <web3.contract.ImplicitContract object at 0x7f9234345a58>

    @pytest.mark.parametrize("contract", contracts)
    def test_updates(accounts, contract):
        c = Controller(contract)
        listeners = [Listener(a, contract) for a in accounts]
        # Have a bunch of random assigments happen
        # See if one of them blows an assert when syncing
        for i in range(10):
            for j, l in enumerate(listeners):
                value = (i*j)
>               c.set(l.acct, value)

test_merkle.py:71: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
model.py:64: in set
    self.tree.set(key, value, self.branch(key))
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/contract.py:976: in __call__
    return super().__call__(*args, transact={})
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/contract.py:893: in __call__
    return self.__prepared_function(*args, **kwargs)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/contract.py:906: in __prepared_function
    return getattr(self._function(*args), modifier)(modifier_dict)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/contract.py:1156: in transact
    **self.kwargs
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/contract.py:1459: in transact_with_contract_function
    txn_hash = web3.eth.sendTransaction(transact_transaction)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/eth.py:263: in sendTransaction
    get_buffered_gas_estimate(self.web3, transaction),
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/utils/transactions.py:84: in get_buffered_gas_estimate
    gas_estimate = web3.eth.estimateGas(gas_estimate_transaction)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/eth.py:304: in estimateGas
    [transaction],
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/manager.py:109: in request_blocking
    response = self._make_request(method, params)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/manager.py:92: in _make_request
    return request_func(method, params)
cytoolz/functoolz.pyx:232: in cytoolz.functoolz.curry.__call__
    ???
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/middleware/formatting.py:50: in apply_formatters
    response = make_request(method, params)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/middleware/gas_price_strategy.py:18: in middleware
    return make_request(method, params)
cytoolz/functoolz.pyx:232: in cytoolz.functoolz.curry.__call__
    ???
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/middleware/formatting.py:48: in apply_formatters
    response = make_request(method, formatted_params)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/middleware/attrdict.py:18: in middleware
    response = make_request(method, params)
cytoolz/functoolz.pyx:232: in cytoolz.functoolz.curry.__call__
    ???
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/middleware/formatting.py:48: in apply_formatters
    response = make_request(method, formatted_params)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/middleware/normalize_errors.py:9: in middleware
    result = make_request(method, params)
cytoolz/functoolz.pyx:232: in cytoolz.functoolz.curry.__call__
    ???
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/middleware/formatting.py:48: in apply_formatters
    response = make_request(method, formatted_params)
cytoolz/functoolz.pyx:232: in cytoolz.functoolz.curry.__call__
    ???
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/middleware/formatting.py:48: in apply_formatters
    response = make_request(method, formatted_params)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/providers/eth_tester/middleware.py:324: in middleware
    return make_request(method, [filled_transaction] + params[1:])
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/middleware/fixture.py:12: in middleware
    return make_request(method, params)
cytoolz/functoolz.pyx:232: in cytoolz.functoolz.curry.__call__
    ???
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/middleware/formatting.py:48: in apply_formatters
    response = make_request(method, formatted_params)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/providers/eth_tester/main.py:46: in make_request
    response = delegator(self.ethereum_tester, params)
cytoolz/functoolz.pyx:232: in cytoolz.functoolz.curry.__call__
    ???
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/web3/providers/eth_tester/defaults.py:36: in call_eth_tester
    return getattr(eth_tester, fn_name)(*fn_args, **fn_kwargs)
/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/eth_tester/main.py:445: in estimate_gas
    raw_gas_estimate = self.backend.estimate_gas(raw_transaction)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = (<eth_tester.backends.pyevm.main.PyEVMBackend object at 0x7f923a015748>, {'data': b'\xf9\x9e\xbe[\x00\x00\x00\x00\x00\...om': b'~_ER\t\x1ai\x12]]\xfc\xb7\xb8\xc2e\x90)9[\xdf', 'to': b')F%\x9e\x034\xf3:\x06A\x060$\x15\xad3\x91\xbe\xd3\x84'})
kwargs = {}

    @functools.wraps(to_wrap)
    def wrapper(*args, **kwargs):
        try:
            return to_wrap(*args, **kwargs)
        except old_exceptions as e:
            try:
>               raise old_to_new_exceptions[type(e)] from e
E               eth_tester.exceptions.TransactionFailed

/home/bryant/.pyenv/versions/3.6.6/envs/merkle-sync/lib/python3.6/site-packages/eth_tester/utils/formatting.py:88: TransactionFailed