diff --git a/.travis.yml b/.travis.yml index a029cab2..597577db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,28 @@ language: python python: - "3.5" -env: - matrix: +matrix: + include: + # lint + - python: "3.5" + env: TOX_POSARGS="-e flake8" # core - - TOX_POSARGS="-e py27-core -e py34-core -e py35-core" - # web3 - - TOX_POSARGS="-e py27-web3v313 -e py34-web3v313 -e py35-web3v313" + - python: "2.7" + env: TOX_POSARGS="-e py27-core" + - python: "3.4" + env: TOX_POSARGS="-e py34-core" + - python: "3.5" + env: TOX_POSARGS="-e py35-core" # pyethereum 1.6.x - - TOX_POSARGS="-e py27-pyethereum16 -e py34-pyethereum16 -e py35-pyethereum16" - # pyethereum 2.0.x - #- TOX_POSARGS="-e py27-pyethereum20 -e py34-pyethereum20 -e py35-pyethereum20" - - TOX_POSARGS="-e flake8" + - python: "2.7" + env: TOX_POSARGS="-e py27-pyethereum16" + - python: "3.4" + env: TOX_POSARGS="-e py34-pyethereum16" + - python: "3.5" + env: TOX_POSARGS="-e py35-pyethereum16" + # pyevm + - python: "3.5" + env: TOX_POSARGS="-e py35-pyevm" cache: pip: true install: diff --git a/README.md b/README.md index 5484f41c..c77a2f5f 100644 --- a/README.md +++ b/README.md @@ -640,9 +640,15 @@ various backends by default. You can however install ethereum tester with the necessary dependencies using the following method. ```bash -$ pip install ethereum-tester[pyethereum16] +$ pip install ethereum-tester[] ``` +You should replace `` with the name of the desired testing +backend. Available backends are: + +* `pyethereum16`: [PyEthereum v1.6.x](https://pypi.python.org/pypi/ethereum/1.6.1) +* `py-evm`: [PyEVM (alpha)](https://pypi.python.org/pypi/py-evm) **(experimental)** + ### Selecting a Backend You can select which backend in a few different ways. @@ -665,36 +671,64 @@ backend class you wish to use. Ethereum tester can be used with the following backends. * PyEthereum 1.6.x (default) +* PyEVM (experimental) +* MockBackend The following backends on the roadmap to be developed. * PyEthereum 2.0.x (under development) -* PyEVM (experimental) -#### PyEthereum 1.6.x +#### MockBackend -TODO +This backend has limited functionality. It cannot perform any VM computations. +It mocks out all of the objects and interactions. -#### PyEthereum 2.0.x (under development) +```python +>>> from eth_tester import MockBackend +>>> t = EthereumTester(MockBackend()) +``` -> Under development +#### PyEthereum 1.6.x + +Uses the PyEthereum library at version `v1.6.x` + +```python +>>> from eth_tester import PyEthereum16Backend +>>> t = EthereumTester(PyEthereum16Backend()) +``` #### PyEVM (experimental) +> **WARNING** Py-EVM is experimental and should not be relied on for mission critical testing at this stage. + +Uses the experimental Py-EVM library. + +```python +>>> from eth_tester import PyEVMBackend +>>> t = EthereumTester(PyEVMBackend()) +``` + +#### PyEthereum 2.0.x (under development) + > Under development ### Implementing Custom Backends The base class `eth_tester.backends.base.BaseChainBackend` is the recommended -base class to begin with if you wish to write your own backend. In order for -ethereum tester to operate correctly, your backend **must** be able to do all -of the following. +base class to begin with if you wish to write your own backend. -TODO +Details on implementation are beyond the scope of this document. ## Data Formats +Ethereum tester uses two formats for data. + +* The *normal* format is the data format the is expected as input arguments to all `EthereumTester` methods as well as the return types from all method calls. +* The *canonical* format is the data format that is used internally by the backend class. + +Ethereum tester enforces strict validation rules on these formats. + ### Canonical Formats The canonical format is intended for low level handling by backends. @@ -756,16 +790,6 @@ The specifics of this object are beyong the scope of this document. # Use with Web3.py -While the `ethereum-tester` library can be used on its own it can also be used -with the [`web3.py`](https://github.com/pipermerriam/web3.py) library. The -`ethereum-tester` library comes with the provider class -`eth_tester.web3.EthereumTesterProvider`. You can use it like this: - -```python ->>> from eth_tester import EthereumTester ->>> from eth_tester.web3 import EthereumTesterProvider ->>> from web3 import Web3 ->>> eth_tester = EthereumTester() ->>> provider = EthereumTesterProvider(eth_tester) ->>> web3 = Web3(provider) -``` +See the [web3.py documentation](http://web3py.readthedocs.io/en/latest/) for +information on the `EthereumTester` provider which integrates with this +library. diff --git a/eth_tester/__init__.py b/eth_tester/__init__.py index be713fc9..3dcfb982 100644 --- a/eth_tester/__init__.py +++ b/eth_tester/__init__.py @@ -6,7 +6,7 @@ from .backends import ( # noqa: F401 MockBackend, PyEthereum16Backend, - PyEthereum20Backend, + PyEVMBackend, ) diff --git a/eth_tester/backends/__init__.py b/eth_tester/backends/__init__.py index 408e3b43..243429c7 100644 --- a/eth_tester/backends/__init__.py +++ b/eth_tester/backends/__init__.py @@ -1,4 +1,7 @@ import os +import sys + +import warnings from eth_tester.utils.module_loading import ( get_import_path, @@ -10,21 +13,36 @@ ) from .pyethereum.v16 import ( PyEthereum16Backend, + is_pyethereum16_available, ) -from .pyethereum.v20 import ( # noqa: F401 - PyEthereum20Backend, +from .pyevm import ( # noqa: F401 + PyEVMBackend, + is_pyevm_available, ) -DEFAULT_CHAIN_BACKEND_CLASS = get_import_path(PyEthereum16Backend) - - def get_chain_backend_class(backend_import_path=None): + warnings.simplefilter('default') + if backend_import_path is None: - backend_import_path = os.environ.get( - 'ETHEREUM_TESTER_CHAIN_BACKEND', - DEFAULT_CHAIN_BACKEND_CLASS, - ) + if 'ETHEREUM_TESTER_CHAIN_BACKEND' in os.environ: + backend_import_path = os.environ['ETHEREUM_TESTER_CHAIN_BACKEND'] + elif is_pyevm_available(): + vi = sys.version_info + if vi.major != 3 or vi.minor < 5: + warnings.warn(UserWarning("Py-EVM does not support python < 3.5")) + backend_import_path = get_import_path(PyEVMBackend) + elif is_pyethereum16_available(): + backend_import_path = get_import_path(PyEthereum16Backend) + else: + warnings.warn(UserWarning( + "Ethereum Tester: No backend was explicitely set, and no *full* " + "backends were available. Falling back to the `MockBackend` " + "which does not support all EVM functionality. Please refer to " + "the `ethereum-tester` documentation for information on what " + "backends are available and how to set them." + )) + backend_import_path = get_import_path(MockBackend) return import_string(backend_import_path) diff --git a/eth_tester/backends/base.py b/eth_tester/backends/base.py index cc359d47..f5b97d90 100644 --- a/eth_tester/backends/base.py +++ b/eth_tester/backends/base.py @@ -23,7 +23,7 @@ def get_fork_block(self, fork_name): # # Meta # - def time_travel(self, timestamp): + def time_travel(self, to_timestamp): raise NotImplementedError("Must be implemented by subclasses") # @@ -38,13 +38,16 @@ def mine_blocks(self, num_blocks=1, coinbase=None): def get_accounts(self): raise NotImplementedError("Must be implemented by subclasses") + def add_account(self, private_key): + raise NotImplementedError("Must be implemented by subclasses") + # # Chain data # - def get_block_by_number(self, block_number): + def get_block_by_number(self, block_number, full_transaction=True): raise NotImplementedError("Must be implemented by subclasses") - def get_block_by_hash(self, block_hash): + def get_block_by_hash(self, block_hash, full_transaction=True): raise NotImplementedError("Must be implemented by subclasses") def get_transaction_by_hash(self, transaction_hash): @@ -74,5 +77,5 @@ def send_transaction(self, transaction): def estimate_gas(self, transaction): raise NotImplementedError("Must be implemented by subclasses") - def call(self, transaction): + def call(self, transaction, block_number="latest"): raise NotImplementedError("Must be implemented by subclasses") diff --git a/eth_tester/backends/mock/main.py b/eth_tester/backends/mock/main.py index c2229857..f46e0e69 100644 --- a/eth_tester/backends/mock/main.py +++ b/eth_tester/backends/mock/main.py @@ -65,12 +65,13 @@ def _get_default_account_data(): } +@to_tuple def get_default_alloc(num_accounts=10): - return { - _generate_dummy_address(idx): _get_default_account_data() - for idx - in range(num_accounts) - } + for idx in range(num_accounts): + yield ( + _generate_dummy_address(idx), + _get_default_account_data(), + ) class MockBackend(BaseChainBackend): @@ -115,6 +116,11 @@ def reset_to_genesis(self): self.block = self.genesis_block self.receipts = {} self.fork_blocks = {} + self.mine_blocks() + + @property + def account_state_lookup(self): + return dict(self.alloc) # # Fork block numbers @@ -160,11 +166,13 @@ def mine_blocks(self, num_blocks=1, coinbase=None): # Accounts # def get_accounts(self): - return tuple(self.alloc.keys()) + return tuple(account for account, _ in self.alloc) def add_account(self, private_key): account = private_key_to_address(private_key) - self.alloc[account] = _get_default_account_data() + self.alloc = self.alloc + ( + (account, _get_default_account_data()), + ) # # Chain data @@ -263,19 +271,19 @@ def get_transaction_receipt(self, transaction_hash): # def get_nonce(self, account, block_number=None): try: - return self.alloc[account]['nonce'] + return self.account_state_lookup[account]['nonce'] except KeyError: return 0 def get_balance(self, account, block_number=None): try: - return self.alloc[account]['balance'] + return self.account_state_lookup[account]['balance'] except KeyError: return 0 def get_code(self, account, block_number=None): try: - return self.alloc[account]['code'] + return self.account_state_lookup[account]['code'] except KeyError: return 0 diff --git a/eth_tester/backends/pyethereum/v16/__init__.py b/eth_tester/backends/pyethereum/v16/__init__.py index 06d6c452..d524744f 100644 --- a/eth_tester/backends/pyethereum/v16/__init__.py +++ b/eth_tester/backends/pyethereum/v16/__init__.py @@ -1,3 +1,8 @@ +from __future__ import absolute_import + +from ..utils import ( # noqa: F401 + is_pyethereum16_available, +) from .main import ( # noqa: F401 PyEthereum16Backend, ) diff --git a/eth_tester/backends/pyethereum/v16/main.py b/eth_tester/backends/pyethereum/v16/main.py index d9338eeb..9de7a18f 100644 --- a/eth_tester/backends/pyethereum/v16/main.py +++ b/eth_tester/backends/pyethereum/v16/main.py @@ -228,6 +228,7 @@ def reset_to_genesis(self): from ethereum import tester self.evm = tester.state() self.evm.extra_accounts = {} + self.mine_blocks() # # Meta diff --git a/eth_tester/backends/pyevm/__init__.py b/eth_tester/backends/pyevm/__init__.py new file mode 100644 index 00000000..1203e622 --- /dev/null +++ b/eth_tester/backends/pyevm/__init__.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import + +from .utils import ( # noqa: F401 + is_pyevm_available, +) +from .main import ( # noqa: F401 + PyEVMBackend, +) diff --git a/eth_tester/backends/pyevm/main.py b/eth_tester/backends/pyevm/main.py new file mode 100644 index 00000000..36d19dbd --- /dev/null +++ b/eth_tester/backends/pyevm/main.py @@ -0,0 +1,391 @@ +from __future__ import absolute_import + +import pkg_resources +import time +import warnings + +from eth_utils import ( + encode_hex, + int_to_big_endian, + pad_left, + to_dict, + to_tuple, + to_wei, + is_integer, +) + +from eth_keys import KeyAPI + +from eth_tester.constants import ( + FORK_HOMESTEAD, + FORK_DAO, + FORK_ANTI_DOS, + FORK_STATE_CLEANUP, +) +from eth_tester.exceptions import ( + BlockNotFound, + TransactionNotFound, + UnknownFork, +) + +from .serializers import ( + serialize_block, + serialize_transaction, + serialize_transaction_receipt, +) +from .utils import is_pyevm_available + + +ZERO_ADDRESS = 20 * b'\x00' +ZERO_HASH32 = 32 * b'\x00' + + +EMPTY_RLP_LIST_HASH = b'\x1d\xccM\xe8\xde\xc7]z\xab\x85\xb5g\xb6\xcc\xd4\x1a\xd3\x12E\x1b\x94\x8at\x13\xf0\xa1B\xfd@\xd4\x93G' # noqa: E501 +BLANK_ROOT_HASH = b'V\xe8\x1f\x17\x1b\xccU\xa6\xff\x83E\xe6\x92\xc0\xf8n\x5bH\xe0\x1b\x99l\xad\xc0\x01b/\xb5\xe3c\xb4!' # noqa: E501 + + +GENESIS_BLOCK_NUMBER = 0 +GENESIS_DIFFICULTY = 131072 +GENESIS_GAS_LIMIT = 3141592 +GENESIS_PARENT_HASH = ZERO_HASH32 +GENESIS_COINBASE = ZERO_ADDRESS +GENESIS_NONCE = b'\x00\x00\x00\x00\x00\x00\x00*' # 42 encoded as big-endian-integer +GENESIS_MIX_HASH = ZERO_HASH32 +GENESIS_EXTRA_DATA = b'' +GENESIS_INITIAL_ALLOC = {} + + +SUPPORTED_FORKS = {FORK_HOMESTEAD, FORK_DAO, FORK_ANTI_DOS} + + +def get_default_account_state(): + return { + 'balance': to_wei(1000000, 'ether'), + 'storage': {}, + 'code': b'', + 'nonce': 0, + } + + +@to_tuple +def get_default_account_keys(): + keys = KeyAPI() + + for i in range(1, 11): + pk_bytes = pad_left(int_to_big_endian(i), 32, b'\x00') + private_key = keys.PrivateKey(pk_bytes) + yield private_key + + +@to_dict +def generate_genesis_state(account_keys): + for private_key in account_keys: + account_state = get_default_account_state() + yield private_key.public_key.to_canonical_address(), account_state + + +def get_default_genesis_params(): + genesis_params = { + "bloom": 0, + "coinbase": GENESIS_COINBASE, + "difficulty": GENESIS_DIFFICULTY, + "extra_data": GENESIS_EXTRA_DATA, + "gas_limit": GENESIS_GAS_LIMIT, + "gas_used": 0, + "mix_hash": GENESIS_MIX_HASH, + "nonce": GENESIS_NONCE, + "block_number": GENESIS_BLOCK_NUMBER, + "parent_hash": GENESIS_PARENT_HASH, + "receipt_root": BLANK_ROOT_HASH, + "timestamp": int(time.time()), + "transaction_root": BLANK_ROOT_HASH, + "uncles_hash": EMPTY_RLP_LIST_HASH + } + return genesis_params + + +def setup_tester_chain(): + from evm.vm.flavors import MainnetTesterChain + from evm.db import get_db_backend + + db = get_db_backend() + genesis_params = get_default_genesis_params() + account_keys = get_default_account_keys() + genesis_state = generate_genesis_state(account_keys) + + chain = MainnetTesterChain.from_genesis(db, genesis_params, genesis_state) + return account_keys, chain + + +def _get_block_by_number(chain, block_number): + if block_number == "latest": + head_block = chain.get_block() + return chain.get_canonical_block_by_number(max(0, head_block.number - 1)) + elif block_number == "earliest": + return chain.get_canonical_block_by_number(0) + elif block_number == "pending": + return chain.get_block() + elif is_integer(block_number): + head_block = chain.get_block() + if block_number == head_block.number: + return head_block + elif block_number < head_block.number: + return chain.get_canonical_block_by_number(block_number) + + # fallback + raise BlockNotFound("No block found for block number: {0}".format(block_number)) + + +def _get_block_by_hash(chain, block_hash): + block = chain.get_block_by_hash(block_hash) + + if block.number >= chain.get_block().number: + raise BlockNotFound("No block fuond for block hash: {0}".format(block_hash)) + + block_at_height = chain.get_canonical_block_by_number(block.number) + if block != block_at_height: + raise BlockNotFound("No block fuond for block hash: {0}".format(block_hash)) + + return block + + +def _get_transaction_by_hash(chain, transaction_hash): + head_block = chain.get_block() + for index, transaction in enumerate(head_block.transactions): + if transaction.hash == transaction_hash: + return head_block, transaction, index + for block_number in range(head_block.number - 1, -1, -1): + # TODO: the chain should be able to look these up directly by hash... + block = chain.get_canonical_block_by_number(block_number) + for index, transaction in enumerate(block.transactions): + if transaction.hash == transaction_hash: + return block, transaction, index + else: + raise TransactionNotFound( + "No transaction found for transaction hash: {0}".format( + encode_hex(transaction_hash) + ) + ) + + +def _execute_and_revert_transaction(chain, transaction, block_number="latest"): + vm = _get_vm_for_block_number(chain, block_number, mutable=True) + + snapshot = vm.snapshot() + computation = vm.execute_transaction(transaction) + vm.revert(snapshot) + return computation + + +def _get_vm_for_block_number(chain, block_number, mutable=False): + block = _get_block_by_number(chain, block_number) + if mutable and not block.header.is_mutable(): + block.header.make_mutable() + vm = chain.get_vm(header=block.header) + return vm + + +class PyEVMBackend(object): + chain = None + fork_blocks = None + + def __init__(self): + self.fork_blocks = {} + + if not is_pyevm_available(): + raise pkg_resources.DistributionNotFound( + "The `py-evm` package is not available. The " + "`PyEVMBackend` requires py-evm to be installed and importable. " + "Please install the `py-evm` library." + ) + + self.reset_to_genesis() + + # + # Private Accounts API + # + @property + def _key_lookup(self): + return { + key.public_key.to_canonical_address(): key + for key + in self.account_keys + } + + # + # Snapshot API + # + def take_snapshot(self): + block = _get_block_by_number(self.chain, 'latest') + return block.hash + + def revert_to_snapshot(self, snapshot): + block = self.chain.get_block_by_hash(snapshot) + header = self.chain.create_header_from_parent(block.header) + self.chain = type(self.chain)(db=self.chain.db, header=header) + + def reset_to_genesis(self): + self.account_keys, self.chain = setup_tester_chain() + + # + # Fork block numbers + # + def set_fork_block(self, fork_name, fork_block): + if fork_name in SUPPORTED_FORKS: + if fork_block: + self.fork_blocks[fork_name] = fork_block + elif fork_name == FORK_STATE_CLEANUP: + warnings.warn(UserWarning( + "Py-EVM does not currently support the SpuriousDragon hard fork." + )) + # TODO: get EIP160 rules implemented in py-evm + self.fork_blocks[fork_name] = fork_block + else: + raise UnknownFork("Unknown fork name: {0}".format(fork_name)) + self.chain.configure_forks() + + def get_fork_block(self, fork_name): + if fork_name in SUPPORTED_FORKS: + return self.fork_blocks.get(fork_name, 0) + elif fork_name == FORK_STATE_CLEANUP: + # TODO: get EIP160 rules implemented in py-evm + return self.fork_blocks.get(fork_name, 0) + else: + raise UnknownFork("Unknown fork name: {0}".format(fork_name)) + + def configure_fork_blocks(self): + self.chain.configure_forks( + homestead=self.fork_blocks.get(FORK_HOMESTEAD), + dao=self.fork_blocks.get(FORK_DAO), + anti_dos=self.fork_blocks.get(FORK_ANTI_DOS), + ) + + # + # Meta + # + def time_travel(self, to_timestamp): + self.chain.header.timestamp = to_timestamp + return to_timestamp + + # + # Mining + # + @to_tuple + def mine_blocks(self, num_blocks=1, coinbase=None): + if coinbase is not None: + mine_kwargs = {'coinbase': coinbase} + else: + mine_kwargs = {} + for _ in range(num_blocks): + block = self.chain.mine_block(**mine_kwargs) + yield block.hash + + # + # Accounts + # + @to_tuple + def get_accounts(self): + for private_key in self.account_keys: + yield private_key.public_key.to_canonical_address() + + def add_account(self, private_key): + keys = KeyAPI() + self.account_keys = self.account_keys + (keys.PrivateKey(private_key),) + + # + # Chain data + # + def get_block_by_number(self, block_number, full_transaction=True): + block = _get_block_by_number(self.chain, block_number) + is_pending = block.number == self.chain.get_block().number + return serialize_block(block, full_transaction, is_pending) + + def get_block_by_hash(self, block_hash, full_transaction=True): + block = _get_block_by_hash(self.chain, block_hash) + is_pending = block.number == self.chain.get_block().number + return serialize_block(block, full_transaction, is_pending) + + def get_transaction_by_hash(self, transaction_hash): + block, transaction, transaction_index = _get_transaction_by_hash( + self.chain, + transaction_hash, + ) + is_pending = block.number == self.chain.get_block().number + return serialize_transaction(block, transaction, transaction_index, is_pending) + + def get_transaction_receipt(self, transaction_hash): + block, transaction, transaction_index = _get_transaction_by_hash( + self.chain, + transaction_hash, + ) + is_pending = block.number == self.chain.get_block().number + return serialize_transaction_receipt(block, transaction, transaction_index, is_pending) + + # + # Account state + # + def get_nonce(self, account, block_number="latest"): + vm = _get_vm_for_block_number(self.chain, block_number) + with vm.state_db(read_only=True) as state_db: + return state_db.get_nonce(account) + + def get_balance(self, account, block_number="latest"): + vm = _get_vm_for_block_number(self.chain, block_number) + with vm.state_db(read_only=True) as state_db: + return state_db.get_balance(account) + + def get_code(self, account, block_number="latest"): + raise NotImplementedError("Must be implemented by subclasses") + + # + # Transactions + # + @to_dict + def _normalize_transaction(self, transaction): + for key in transaction: + if key == 'from': + continue + yield key, transaction[key] + if 'nonce' not in transaction: + yield 'nonce', self.get_nonce(transaction['from']) + if 'data' not in transaction: + yield 'data', b'' + if 'gas_price' not in transaction: + yield 'gas_price', 1 + if 'value' not in transaction: + yield 'value', 0 + if 'to' not in transaction: + yield 'to', b'' + + def _get_normalized_and_signed_evm_transaction(self, transaction): + signing_key = self._key_lookup[transaction['from']] + normalized_transaction = self._normalize_transaction(transaction) + evm_transaction = self.chain.create_unsigned_transaction(**normalized_transaction) + signed_evm_transaction = evm_transaction.as_signed_transaction(signing_key) + return signed_evm_transaction + + def send_transaction(self, transaction): + signed_evm_transaction = self._get_normalized_and_signed_evm_transaction( + transaction, + ) + self.chain.apply_transaction(signed_evm_transaction) + return signed_evm_transaction.hash + + def estimate_gas(self, transaction): + # TODO: move this to the VM level (and use binary search approach) + signed_evm_transaction = self._get_normalized_and_signed_evm_transaction( + transaction, + ) + + computation = _execute_and_revert_transaction(self.chain, signed_evm_transaction) + + return computation.gas_meter.start_gas - computation.gas_meter.gas_remaining + + def call(self, transaction, block_number="latest"): + # TODO: move this to the VM level. + signed_evm_transaction = self._get_normalized_and_signed_evm_transaction( + transaction, + ) + + computation = _execute_and_revert_transaction(self.chain, signed_evm_transaction) + return computation.output diff --git a/eth_tester/backends/pyevm/serializers.py b/eth_tester/backends/pyevm/serializers.py new file mode 100644 index 00000000..2ea8ffe9 --- /dev/null +++ b/eth_tester/backends/pyevm/serializers.py @@ -0,0 +1,126 @@ +import rlp + +from cytoolz import ( + partial, +) + +from eth_utils import ( + pad_left, + to_canonical_address, +) + +from eth_tester.utils.address import ( + generate_contract_address, +) +from eth_tester.utils.encoding import ( + int_to_32byte_big_endian, +) + + +pad32 = partial(pad_left, to_size=32, pad_with=b'\x00') + + +def serialize_block(block, full_transaction, is_pending): + if full_transaction: + transaction_serializer = serialize_transaction + else: + transaction_serializer = serialize_transaction_hash + + transactions = [ + transaction_serializer(block, transaction, index, is_pending) + for index, transaction + in enumerate(block.transactions) + ] + + if block.uncles: + raise NotImplementedError("Uncle serialization has not been implemented") + + return { + "number": block.header.block_number, + "hash": block.header.hash, + "parent_hash": block.header.parent_hash, + "nonce": block.header.nonce, + "sha3_uncles": block.header.uncles_hash, + "logs_bloom": block.header.bloom, + "transactions_root": block.header.transaction_root, + "receipts_root": block.header.receipt_root, + "state_root": block.header.state_root, + "miner": block.header.coinbase, + "difficulty": block.header.difficulty, + "total_difficulty": block.header.difficulty, # TODO: actual total difficulty + "size": len(rlp.encode(block)), + "extra_data": pad32(block.header.extra_data), + "gas_limit": block.header.gas_limit, + "gas_used": block.header.gas_used, + "timestamp": block.header.timestamp, + "transactions": transactions, + "uncles": [uncle.hash for uncle in block.uncles], + } + + +def serialize_transaction_hash(block, transaction, transaction_index, is_pending): + return transaction.hash + + +def serialize_transaction(block, transaction, transaction_index, is_pending): + return { + "hash": transaction.hash, + "nonce": transaction.nonce, + "block_hash": None if is_pending else block.hash, + "block_number": None if is_pending else block.number, + "transaction_index": None if is_pending else transaction_index, + "from": transaction.sender, + "to": transaction.to, + "value": transaction.value, + "gas": transaction.gas, + "gas_price": transaction.gas_price, + "data": transaction.data, + "v": transaction.v, + "r": transaction.r, + "s": transaction.s, + } + + +def serialize_transaction_receipt(block, transaction, transaction_index, is_pending): + receipt = block.receipts[transaction_index] + + if transaction.to == b'': + contract_addr = to_canonical_address(generate_contract_address( + transaction.sender, + transaction.nonce, + )) + else: + contract_addr = None + + if transaction_index == 0: + origin_gas = 0 + else: + origin_gas = receipt.gas_used - block.receipts[transaction_index - 1].gas_used + + return { + "transaction_hash": transaction.hash, + "transaction_index": None if is_pending else transaction_index, + "block_number": None if is_pending else block.number, + "block_hash": None if is_pending else block.hash, + "cumulative_gas_used": receipt.gas_used, + "gas_used": receipt.gas_used - origin_gas, + "contract_address": contract_addr, + "logs": [ + serialize_log(block, transaction, transaction_index, log, log_index, is_pending) + for log_index, log in enumerate(receipt.logs) + ], + } + + +def serialize_log(block, transaction, transaction_index, log, log_index, is_pending): + return { + "type": "pending" if is_pending else "mined", + "log_index": log_index, + "transaction_index": None if is_pending else transaction_index, + "transaction_hash": transaction.hash, + "block_hash": None if is_pending else block.hash, + "block_number": None if is_pending else block.number, + "address": log.address, + "data": log.data, + "topics": [int_to_32byte_big_endian(topic) for topic in log.topics], + } diff --git a/eth_tester/backends/pyevm/utils.py b/eth_tester/backends/pyevm/utils.py new file mode 100644 index 00000000..8a072811 --- /dev/null +++ b/eth_tester/backends/pyevm/utils.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import + +import pkg_resources + +from semantic_version import ( + Version, +) + + +def get_pyevm_version(): + try: + base_version = pkg_resources.parse_version( + pkg_resources.get_distribution("py-evm").version + ).base_version + return Version(base_version) + except pkg_resources.DistributionNotFound: + return None + + +def is_pyevm_available(): + pyevm_version = get_pyevm_version() + + if pyevm_version is None: + return False + else: + return True diff --git a/eth_tester/main.py b/eth_tester/main.py index d145e10f..fe7f433d 100644 --- a/eth_tester/main.py +++ b/eth_tester/main.py @@ -97,7 +97,9 @@ def __init__(self, self.normalizer = normalizer self.auto_mine_transactions = auto_mine_transactions - self.fork_blocks = fork_blocks + + for fork_name, fork_block in fork_blocks.items(): + self.set_fork_block(fork_name, fork_block) self._reset_local_state() @@ -116,10 +118,6 @@ def __init__(self, _account_unlock = None def _reset_local_state(self): - # fork blocks - for fork_name, fork_block in self.fork_blocks.items(): - self.set_fork_block(fork_name, fork_block) - # filter tracking self._filter_counter = itertools.count() self._log_filters = {} diff --git a/eth_tester/utils/address.py b/eth_tester/utils/address.py new file mode 100644 index 00000000..0f7070b9 --- /dev/null +++ b/eth_tester/utils/address.py @@ -0,0 +1,9 @@ +import rlp + +from eth_utils import ( + keccak, +) + + +def generate_contract_address(address, nonce): + return keccak(rlp.encode([address, nonce]))[-20:] diff --git a/eth_tester/utils/backend_testing.py b/eth_tester/utils/backend_testing.py index 9805814c..9c409a13 100644 --- a/eth_tester/utils/backend_testing.py +++ b/eth_tester/utils/backend_testing.py @@ -66,6 +66,38 @@ } +BLOCK_KEYS = { + "number", + "hash", + "parent_hash", + "nonce", + "sha3_uncles", + "logs_bloom", + "transactions_root", + "receipts_root", + "state_root", + "miner", + "difficulty", + "total_difficulty", + "size", + "extra_data", + "gas_limit", + "gas_used", + "timestamp", + "transactions", + "uncles", +} + + +def _validate_serialized_block(block): + missing_keys = BLOCK_KEYS.difference(block.keys()) + if missing_keys: + error_message = "Serialized block is missing the following keys: {0}".format( + "|".join(sorted(missing_keys)), + ) + raise AssertionError(error_message) + + class BaseTestBackendDirect(object): # # Utils @@ -225,18 +257,23 @@ def test_auto_mine_transactions_disabled(self, eth_tester): def test_get_genesis_block_by_number(self, eth_tester): block = eth_tester.get_block_by_number(0) assert block['number'] == 0 + _validate_serialized_block(block) def test_get_genesis_block_by_hash(self, eth_tester): genesis_hash = eth_tester.get_block_by_number(0)['hash'] block = eth_tester.get_block_by_hash(genesis_hash) assert block['number'] == 0 + _validate_serialized_block(block) def test_get_block_by_number(self, eth_tester): + origin_block_number = eth_tester.get_block_by_number('pending')['number'] mined_block_hashes = eth_tester.mine_blocks(10) - for block_number, block_hash in enumerate(mined_block_hashes): + for offset, block_hash in enumerate(mined_block_hashes): + block_number = origin_block_number + offset block = eth_tester.get_block_by_number(block_number) assert block['number'] == block_number assert block['hash'] == block_hash + _validate_serialized_block(block) def test_get_block_by_number_full_transactions(self, eth_tester): eth_tester.mine_blocks(2) @@ -267,8 +304,11 @@ def test_get_block_by_number_only_transaction_hashes(self, eth_tester): assert is_hex(block['transactions'][0]) def test_get_block_by_hash(self, eth_tester): + origin_block_number = eth_tester.get_block_by_number('pending')['number'] + mined_block_hashes = eth_tester.mine_blocks(10) - for block_number, block_hash in enumerate(mined_block_hashes): + for offset, block_hash in enumerate(mined_block_hashes): + block_number = origin_block_number + offset block = eth_tester.get_block_by_hash(block_hash) assert block['number'] == block_number assert block['hash'] == block_hash @@ -311,19 +351,22 @@ def test_get_block_by_latest_unmined_genesis(self, eth_tester): assert block['number'] == 0 def test_get_block_by_latest_only_genesis(self, eth_tester): - eth_tester.mine_blocks() block = eth_tester.get_block_by_number('latest') assert block['number'] == 0 def test_get_block_by_latest(self, eth_tester): + origin_block_number = eth_tester.get_block_by_number('pending')['number'] + eth_tester.mine_blocks(10) block = eth_tester.get_block_by_number('latest') - assert block['number'] == 9 + assert block['number'] == 9 + origin_block_number def test_get_block_by_pending(self, eth_tester): + origin_block_number = eth_tester.get_block_by_number('pending')['number'] + eth_tester.mine_blocks(10) block = eth_tester.get_block_by_number('pending') - assert block['number'] == 10 + assert block['number'] == 10 + origin_block_number # Transactions def test_get_transaction_by_hash(self, eth_tester): @@ -400,34 +443,37 @@ def test_estimate_gas(self, eth_tester): # Snapshot and Revert # def test_genesis_snapshot_and_revert(self, eth_tester): - assert eth_tester.get_block_by_number('latest')['number'] == 0 - assert eth_tester.get_block_by_number('pending')['number'] == 0 + origin_latest = eth_tester.get_block_by_number('latest')['number'] + origin_pending = eth_tester.get_block_by_number('pending')['number'] + snapshot_id = eth_tester.take_snapshot() # now mine 10 blocks in eth_tester.mine_blocks(10) - assert eth_tester.get_block_by_number('latest')['number'] == 9 - assert eth_tester.get_block_by_number('pending')['number'] == 10 + assert eth_tester.get_block_by_number('latest')['number'] == origin_latest + 10 + assert eth_tester.get_block_by_number('pending')['number'] == origin_pending + 10 eth_tester.revert_to_snapshot(snapshot_id) - assert eth_tester.get_block_by_number('latest')['number'] == 0 - assert eth_tester.get_block_by_number('pending')['number'] == 0 + assert eth_tester.get_block_by_number('latest')['number'] == origin_latest + assert eth_tester.get_block_by_number('pending')['number'] == origin_pending def test_snapshot_and_revert_post_genesis(self, eth_tester): eth_tester.mine_blocks(5) - assert eth_tester.get_block_by_number('latest')['number'] == 4 - assert eth_tester.get_block_by_number('pending')['number'] == 5 + origin_latest = eth_tester.get_block_by_number('latest')['number'] + origin_pending = eth_tester.get_block_by_number('pending')['number'] + snapshot_id = eth_tester.take_snapshot() # now mine 10 blocks in eth_tester.mine_blocks(10) - assert eth_tester.get_block_by_number('latest')['number'] == 14 - assert eth_tester.get_block_by_number('pending')['number'] == 15 + assert eth_tester.get_block_by_number('latest')['number'] == origin_latest + 10 + assert eth_tester.get_block_by_number('pending')['number'] == origin_pending + 10 eth_tester.revert_to_snapshot(snapshot_id) - assert eth_tester.get_block_by_number('latest')['number'] == 4 - assert eth_tester.get_block_by_number('pending')['number'] == 5 + + assert eth_tester.get_block_by_number('latest')['number'] == origin_latest + assert eth_tester.get_block_by_number('pending')['number'] == origin_pending def test_revert_cleans_up_invalidated_pending_block_filters(self, eth_tester): # first mine 10 blocks in @@ -616,15 +662,17 @@ def _emit(v): assert len(after_all) == 6 def test_reset_to_genesis(self, eth_tester): + origin_latest = eth_tester.get_block_by_number('latest')['number'] + origin_pending = eth_tester.get_block_by_number('pending')['number'] eth_tester.mine_blocks(5) - assert eth_tester.get_block_by_number('latest')['number'] == 4 - assert eth_tester.get_block_by_number('pending')['number'] == 5 + assert eth_tester.get_block_by_number('latest')['number'] == origin_latest + 5 + assert eth_tester.get_block_by_number('pending')['number'] == origin_pending + 5 eth_tester.reset_to_genesis() - assert eth_tester.get_block_by_number('latest')['number'] == 0 - assert eth_tester.get_block_by_number('pending')['number'] == 0 + assert eth_tester.get_block_by_number('latest')['number'] == origin_latest + assert eth_tester.get_block_by_number('pending')['number'] == origin_pending # # Filters diff --git a/setup.py b/setup.py index 9a53f40b..4ab932b0 100644 --- a/setup.py +++ b/setup.py @@ -28,11 +28,15 @@ "ethereum-utils>=0.3.1", "rlp==0.5.1", "semantic_version>=2.6.0", + "ethereum-keys>=0.1.0-alpha.7", ], extras_require={ 'pyethereum16': [ "ethereum>=1.6.0,<2.0.0", ], + 'py-evm': [ + "py-evm==0.2.0a5", + ], }, py_modules=['eth_tester'], license="MIT", diff --git a/tests/backends/test_pyethereum20.py b/tests/backends/test_pyethereum20.py deleted file mode 100644 index dca15fd3..00000000 --- a/tests/backends/test_pyethereum20.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import unicode_literals - -import pytest - -from eth_utils import is_address -from eth_tester import ( - EthereumTester, - PyEthereum20Backend, -) - -from eth_tester.backends.pyethereum.utils import ( - is_pyethereum20_available, -) - -from eth_tester.utils.backend_testing import ( - BaseTestBackendDirect, - BaseTestBackendFuzz, -) - - -@pytest.fixture -def eth_tester(): - if not is_pyethereum20_available(): - pytest.skip("PyEthereum >=2.0.0,<2.1.0 not available") - backend = PyEthereum20Backend() - return EthereumTester(backend=backend) - - -class TestPyEthereum20BackendDirect(BaseTestBackendDirect): - pass - - -class TestPyEthereum20BackendFuzz(BaseTestBackendFuzz): - pass diff --git a/tests/backends/test_pyevm.py b/tests/backends/test_pyevm.py new file mode 100644 index 00000000..5b9c578a --- /dev/null +++ b/tests/backends/test_pyevm.py @@ -0,0 +1,28 @@ +from __future__ import unicode_literals + +import pytest + +from eth_tester import ( + EthereumTester, + PyEVMBackend, +) + +from eth_tester.backends.pyevm.utils import ( + is_pyevm_available, +) + +from eth_tester.utils.backend_testing import ( + BaseTestBackendDirect, +) + + +@pytest.fixture +def eth_tester(): + if not is_pyevm_available(): + pytest.skip("PyEVM is not available") + backend = PyEVMBackend() + return EthereumTester(backend=backend) + + +class TestPyEVMBackendDirect(BaseTestBackendDirect): + pass diff --git a/tests/backends/test_mock_backend.py b/tests/core/test_mock_backend.py similarity index 100% rename from tests/backends/test_mock_backend.py rename to tests/core/test_mock_backend.py diff --git a/tox.ini b/tox.ini index f0cc979c..77ad778b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,7 @@ [tox] envlist= py{27,34,35}-{core} - py{27,34,35}-{pyethereum16,pyethereum20} - py{27,34,35}-{web3v313} + py{27,34,35}-{pyethereum16,pyethereum20,pyevm} flake8 [flake8] @@ -13,12 +12,15 @@ exclude= tests/* usedevelop=True commands= core: py.test {posargs:tests/core} - pyethereum{16,20}: py.test {posargs:tests/backends} + pyethereum16: py.test {posargs:tests/backends/test_pyethereum16.py} + pyethereum20: py.test {posargs:tests/backends/test_pyethereum20.py} + pyevm: py.test {posargs:tests/backends/test_pyevm.py} deps = -r{toxinidir}/requirements-dev.txt + coincurve>=6.0.0 pyethereum16: ethereum>=1.6.0,<1.7.0 pyethereum20: ethereum>=2.0.0,<2.1.0 - web3: web3v313==3.13.4,<3.14.0 + pyevm: py-evm==0.2.0a5 py27: mock==2.0.0 basepython = py27: python2.7