ethereum / py-evm

A Python implementation of the Ethereum Virtual Machine
https://py-evm.readthedocs.io/en/latest/
MIT License
2.27k stars 655 forks source link

"Invalid opcode 0x5e" when emitting an event #2158

Closed fjarri closed 8 months ago

fjarri commented 8 months ago

Macos 14.3.1, Python 3.10.10, py-evm 0.10.0b2

Trying to call a method of a compiled contract, getting an "invalid opcode" error from the VM. Solidity compiler version is set to 0.8.25 in solcx.

Full reproducing example:

import tempfile

import rlp
import solcx
from eth_account import Account
from eth_keys import KeyAPI
from eth_utils import keccak, to_canonical_address

from eth import constants
from eth.chains.base import MiningChain
from eth.db.atomic import AtomicDB
from eth.vm.forks.shanghai import ShanghaiVM
from eth.vm.spoof import SpoofTransaction

def compile_contract():
    src = """
pragma solidity ^0.8;

contract Test {
    event MyEvent(
        bytes x
    ) anonymous;

    function emitEvent() public {
        bytes memory bytestring = "0123456789";
        emit MyEvent(bytestring);
    }
}
    """

    with tempfile.NamedTemporaryFile(mode="w") as f:
        f.file.write(src)
        f.file.close()
        compiled = solcx.compile_files([f.name], output_values=["bin", "asm"], solc_version="0.8.25")

    return bytes.fromhex(compiled[f.name + ":Test"]["bin"])

def make_chain():
    klass = MiningChain.configure(vm_configuration=((0, ShanghaiVM),))

    genesis_params = {
        "coinbase": constants.ZERO_ADDRESS,
        "transaction_root": constants.BLANK_ROOT_HASH,
        "receipt_root": constants.BLANK_ROOT_HASH,
        "difficulty": 0,
        "gas_limit": 30029122,
        "timestamp": 0,
        "extra_data": constants.GENESIS_EXTRA_DATA,
        "nonce": b"\x00" * 8,
    }

    # This seems to be hardcoded in PyEVM somehow.
    root_private_key = KeyAPI().PrivateKey(b"\x00" * 31 + b"\x01")
    account = Account.from_key(root_private_key)

    genesis_state = {
        to_canonical_address(account.address): {
            "balance": 100 * 10**18,
            "storage": {},
            "code": b"",
            "nonce": 0,
        }
    }

    chain = klass.from_genesis(AtomicDB(), genesis_params, genesis_state)

    return account, chain

def main():
    account, chain = make_chain()

    contract_data = compile_contract()

    # Deploy the contract

    block = chain.get_block()
    header = block.header
    evm_transaction = chain.create_unsigned_transaction(
        gas_price=0, gas=30029122, nonce=0, value=0, data=contract_data, to=b""
    )
    spoofed_transaction = SpoofTransaction(
        evm_transaction, from_=to_canonical_address(account.address)
    )
    gas = chain.estimate_gas(spoofed_transaction, header)

    gas_price = header.base_fee_per_gas + 10**9

    tx = {
        "chainId": "0x1",
        "value": "0x0",
        "gas": hex(gas),
        "maxFeePerGas": hex(gas_price),
        "maxPriorityFeePerGas": hex(10**9),
        "nonce": "0x0",
        "data": "0x" + contract_data.hex(),
        "type": "0x2",
    }
    signed_tx = account.sign_transaction(tx).rawTransaction

    vm = chain.get_vm(at_header=header)
    tx = vm.get_transaction_builder().decode(signed_tx)
    chain.apply_transaction(tx)
    chain.mine_block()

    contract_address = keccak(rlp.encode([to_canonical_address(account.address), 0]))[-20:]

    # Transact

    header = chain.get_block().header
    evm_transaction = chain.create_unsigned_transaction(
        gas_price=0,
        gas=30029122,
        nonce=1,
        value=0,
        data=keccak(b"emitEvent()")[:4],
        to=contract_address,
    )
    spoofed_transaction = SpoofTransaction(
        evm_transaction, from_=to_canonical_address(account.address)
    )
    chain.estimate_gas(spoofed_transaction, header)

if __name__ == "__main__":
    main()

Output:

Traceback (most recent call last):
  File "/Users/bogdan/wb/repos/py-evm/test.py", line 128, in <module>
    main()
  File "/Users/bogdan/wb/repos/py-evm/test.py", line 124, in main
    chain.estimate_gas(spoofed_transaction, header)
  File "/Users/bogdan/wb/repos/py-evm/eth/chains/base.py", line 491, in estimate_gas
    return self.gas_estimator(state, transaction)
  File "cytoolz/functoolz.pyx", line 263, in cytoolz.functoolz.curry.__call__
  File "/Users/bogdan/wb/repos/py-evm/eth/estimators/gas.py", line 89, in binary_gas_search
    raise error
  File "/Users/bogdan/wb/repos/py-evm/eth/vm/computation.py", line 409, in apply_computation
    opcode_fn(computation=computation)
  File "/Users/bogdan/wb/repos/py-evm/eth/vm/logic/invalid.py", line 21, in __call__
    raise InvalidInstruction(
eth.exceptions.InvalidInstruction: Invalid opcode 0x5e @ 167
Alleysira commented 8 months ago

According to the information provided on https://www.evm.codes/?fork=cancun, the opcode MCOPY(0X5E) was introduced in the Cancun fork. However, in your code snippet:

klass = MiningChain.configure(vm_configuration=((0, ShanghaiVM),))

you are using the ShanghaiVM, where 0x5e is not defined. To address the issue, you may consider either using an older version of the compiler to escape using new opcode or a newer version of the VM that supports the MCOPY opcode.

fselmo commented 8 months ago

@Alleysira has the right idea. You'll need to use the CancunVM from the latest available version of py-evm in order to support MCOPY.